|
Home Pages Pidgin Azarennya (S|N) Mac Thesaurus Reference ToDo Colino Food Local Blogs: BadIdea Rachel RIAA Cult: Clambake Infidels Fi: Arda StarTrek Trek/Wars Film: IMDB D Harry Jabootu Kyle Fun: Agony ICanHas? ObSkills Snopes Lang: ZBB Vreleksá AwkWords Omniglot Scriptorium More... Local: Maps Map MyWeb Metro (map) FC Weather GoWhere? GGWash DC Arlington Reston Beyond Bacon Pix: Deviant Places Renderosity Blender Artists Pol: Anchoress Lizards Lucianne Strata WAwakes Sci: SmallThings Darwin AntiEvo Skeptics EvC BAUT Physics /.Sci Junk Panda Pharyngula Mags AmSci NatG Space X86: OSX86 ArsTech OSNews TUAW Dev PowWeb PHP Webmaster Coding Walkers Prog: PHP JS Toolbox Unobt Compress RegExp (test) Lint SQL Cocoa Builder Dev Apple BBS Userland Faqin Science/Tech: Engadget Thunderbolts Icecap Centauri NewSci Gizmodo co2sci ClimateDebate SciDaily Nrich NatGeog Math CreatClaims GoodBadMath CurrentEvents: OrigSig Flamingo FlopAces ImmigProf ~J~ MyVRWC NewsGroper Pal2Pal Sanity Simon TCS Toldjah Blogs... Tools: Calculator AsciiArt XMLVal FunStuff: Pictures: Photobucket (eg Dubai) Videos: YouTube Subtitler InterestingThings: LibraryThing FlashCards GoogleDocs Wowio Bubbl.us Colemak Audible PodioBooks WonderfulInfo BooksOnline AboutUs.org |
Main /
TextEditSourceCodeController.h
#import <Cocoa/Cocoa.h>
@interface Controller : NSObject {
IBOutlet NSPanel *propertiesPanel;
IBOutlet id keywordsField;
}
/* NSApplication delegate methods */
- (void)application:(NSApplication *)app printFiles:(NSArray *)filenames;
- (void)application:(NSApplication *)app openFiles:(NSArray *)filenames;
- (BOOL)applicationOpenUntitledFile:(NSApplication *)app;
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)app;
/* Action methods */
- (void)createNew:(id)sender;
- (void)open:(id)sender;
- (void)saveAll:(id)sender;
@end
Controller.m
/*
Controller.m
Copyright (c) 1995-2005 by Apple Computer, Inc., all rights reserved.
Author: Ali Ozer
TextEdit milestones:
Initially created 1/28/95
Multiple page support 2/16/95
Preferences panel 10/24/95
HTML 7/3/97
Exported services 8/1/97
Java version created 8/11/97
Undo 9/17/97
Scripting 6/18/98
Aquafication 11/1/99
Encoding customization 5/20/02
TODO: Use URLs, switch to NSDocument
Central controller object for TextEdit.
*/
#import <Cocoa/Cocoa.h>
#import "Controller.h"
#import "Document.h"
#import "Preferences.h"
static NSString *fullPathOfAppForType(NSString *type); // Return app to open docs of a given extension
@implementation Controller
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
// To get service requests to go to the controller...
[NSApp setServicesProvider:self];
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)app {
NSArray *windows = [app windows];
unsigned count = [windows count];
unsigned needsSaving = 0;
// Determine if there are any unsaved documents...
while (count--) {
NSWindow *window = [windows objectAtIndex:count];
Document *document = [Document documentForWindow:window];
if (document && [document isDocumentEdited]) needsSaving++;
}
if (needsSaving > 0) {
int choice = NSAlertDefaultReturn; // Meaning, review changes
if (needsSaving > 1) { // If we only have 1 unsaved document, we skip the "review changes?" panel
NSString *title = [NSString stringWithFormat:NSLocalizedString(@"You have %d documents with unsaved changes. Do you want to review these changes before quitting?",
@"Title of alert panel which comes up when user chooses Quit and there are multiple unsaved documents."), needsSaving];
choice = NSRunAlertPanel(title,
NSLocalizedString(@"If you don\\U2019t review your documents, all your changes will be lost.",
@"Warning in the alert panel which comes up when user chooses Quit and there are unsaved documents."),
NSLocalizedString(@"Review Changes\\U2026",
@"Choice (on a button) given to user which allows him/her to review all unsaved documents if he/she quits the application without saving them all first."), // ellipses
NSLocalizedString(@"Discard Changes",
@"Choice (on a button) given to user which allows him/her to quit the application even though there are unsaved documents."),
NSLocalizedString(@"Cancel", @"Button choice allowing user to cancel."));
if (choice == NSAlertOtherReturn) return NSTerminateCancel; /* Cancel */
}
if (choice == NSAlertDefaultReturn) { /* Review unsaved; Quit Anyway falls through */
[Document reviewChangesAndQuitEnumeration:YES];
return NSTerminateLater;
}
}
return NSTerminateNow;
}
- (void)applicationWillTerminate:(NSNotification *)notification {
[Preferences saveDefaults];
}
- (BOOL)openFiles:(NSArray *)filenames {
BOOL someSuccess = YES;
NSMutableArray *openFailures = nil;
Document *previousDocument = nil; // We open multiply docs open front-to-back, for performance reasons
int cnt = 0, numFilenames = [filenames count];
while (cnt < numFilenames) { // Loop over all filenames
NSAutoreleasePool *pool = [NSAutoreleasePool new]; // Because there might be many filenames, use a local pool to limit high water mark
BOOL success;
NSError *error = nil;
NSString *path = [filenames objectAtIndex:cnt];
// Now for a unfortunate hack to see if we're being force-fed screen grabs from 9, which have SimpleText's app signature...
// If the file has type PICT and creator ttxt, we should open it in an image viewer. Note that this diversion is an issue
// only for TextEdit, which duplicates SimpleText's app signature.
NSDictionary *attributes = [[NSFileManager defaultManager] fileAttributesAtPath:path traverseLink:YES];
if (attributes && ([attributes fileHFSTypeCode] == 'PICT') && ([attributes fileHFSCreatorCode] == 'ttxt')) {
NSString *app = fullPathOfAppForType(@"pict");
// If we get back ourselves (TextEdit) or nothing, hardwire to use Preview
if (!app || [app isEqual:[[NSBundle mainBundle] bundlePath]])
app = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:@"com.apple.Preview"];
success = app && [[NSWorkspace sharedWorkspace] openFile:path withApplication:app];
} else {
Document *document = [Document openDocumentWithPath:path encoding:[[Preferences objectForKey:PlainTextEncodingForRead] intValue] behind:previousDocument error:&error];
success = document ? YES : NO;
if (success) previousDocument = document;
}
if (!success) {
if (!error) error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:[NSDictionary dictionaryWithObject:path forKey:NSFilePathErrorKey]];
// If no error came back for some reason, create a generic one
if (!openFailures) openFailures = [[NSMutableArray alloc] init];
[openFailures addObject:error];
}
cnt++;
[pool release];
}
if (openFailures) {
someSuccess = [openFailures count] < [filenames count];
[Document displayOpenFailures:openFailures someSucceeded:someSuccess];
[openFailures release];
}
return someSuccess;
}
/* Note, if your application needs to run on pre-Tiger systems, you should also implement application:openFile:
*/
- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames {
BOOL success = [self openFiles:filenames];
[NSApp replyToOpenOrPrint:success ? NSApplicationDelegateReplySuccess : NSApplicationDelegateReplyFailure];
}
- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender {
return [Document openUntitled:YES] ? YES : NO;
}
- (void)application:(NSApplication *)sender printFiles:(NSArray *)filenames {
BOOL printingCancelled = NO;
int cnt = 0, numFilenames = [filenames count];
while ((cnt < numFilenames) && !printingCancelled) {
BOOL releaseDoc = NO;
NSString *path = [filenames objectAtIndex:cnt];
Document *document = [Document documentForPath:path];
if (!document) {
document = [[Document alloc] initWithPath:path encoding:[[Preferences objectForKey:PlainTextEncodingForRead] intValue] uniqueZone:NO error:NULL];
releaseDoc = YES; // Indicating this document should be closed after printing
}
if (document) {
printingCancelled = ![document printDocumentModally:YES];
if (releaseDoc) [document release]; // If we created it, we get rid of it.
}
cnt++;
}
[NSApp replyToOpenOrPrint:printingCancelled ? NSApplicationDelegateReplyCancel : NSApplicationDelegateReplySuccess];
}
- (void)createNew:(id)sender {
(void)[Document openUntitled:NO];
}
- (void)open:(id)sender {
[Document open:sender];
}
- (void)saveAll:(id)sender {
[Document saveAllEnumeration:YES];
}
/*** Properties panel support ***/
- (void)togglePropertiesPanel:(id)sender {
if (!propertiesPanel) {
if (![NSBundle loadNibNamed:@"DocumentProperties" owner:self]) {
NSLog(@"Failed to load DocumentProperties.nib");
return;
}
}
if ([propertiesPanel isVisible]) {
[propertiesPanel orderOut:sender];
} else {
[propertiesPanel makeKeyAndOrderFront:sender];
}
}
/* validateMenuItem: is used to dynamically set attributes of menu items.
*/
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
if ([menuItem action] == @selector(togglePropertiesPanel:)) { // Correctly toggle the menu item for showing/hiding document properties
validateToggleItem(menuItem, [propertiesPanel isVisible], NSLocalizedString(@"Hide Properties", @"Title for menu item to hide the document properties panel."),
NSLocalizedString(@"Show Properties",
@"Title for menu item to show the document properties panel (should be the same as the initial menu item in the nib)."));
}
return YES;
}
/*** Services support ***/
- (void)openFile:(NSPasteboard *)pboard userData:(NSString *)data error:(NSString **)error {
NSArray *types = [pboard types];
NSString *filename, *origFilename;
if ([types containsObject:NSStringPboardType] && (filename = origFilename = [pboard stringForType:NSStringPboardType])) {
BOOL success = NO;
if ([filename isAbsolutePath]) { // If seems to be a valid absolute path, first try using it as-is
success = [Document openDocumentWithPath:filename encoding:[[Preferences objectForKey:PlainTextEncodingForRead] intValue] behind:nil error:NULL] ? YES : NO;
}
if (!success) { // Check to see if the user mistakenly included a carriage return or more at the end of the file name...
filename = [[filename substringWithRange:[filename lineRangeForRange:NSMakeRange(0, 0)]] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([filename hasPrefix:@"~"]) filename = [filename stringByExpandingTildeInPath]; /* Convert the "~username" case */
if (![origFilename isEqual:filename] && [filename isAbsolutePath]) success = [Document openDocumentWithPath:filename encoding:[[Preferences objectForKey:PlainTextEncodingForRead] intValue] behind:nil error:NULL] ? YES : NO;
}
// Given that this is a one-way service (no return), we need to put up the error panel ourselves and we do not set *error.
if (!success) {
if ([filename length] > PATH_MAX + 10) filename = [[filename substringToIndex:PATH_MAX] stringByAppendingString:@"... "];
(void)NSRunAlertPanel(NSLocalizedString(@"Open File Failed", @"Title of alert indicating error during Open File service"),
NSLocalizedString(@"Couldn\\U2019t open file \\U201c%@\\U201d.", @"Message indicating file couldn't be opened; %@ is the filename."),
NSLocalizedString(@"OK", @"OK"), nil, nil, filename);
}
}
}
- (void)openSelection:(NSPasteboard *)pboard userData:(NSString *)data error:(NSString **)error {
BOOL success = NO;
Document *document = [Document openUntitled:NO];
NSArray *types = [pboard types];
NSString *preferredType = [[document firstTextView] preferredPasteboardTypeFromArray:types restrictedToTypesFromArray:nil];
if (preferredType) {
[document setRichText:![preferredType isEqualToString:NSStringPboardType]]; /* Special case to open a plain text document */
success = [[document firstTextView] readSelectionFromPasteboard:pboard type:preferredType];
[document setDocumentName:nil];
}
if (!success) {
(void)NSRunAlertPanel(NSLocalizedString(@"Open Selection Failed", @"Title of alert indicating error during Open Selection service"),
NSLocalizedString(@"Couldn\\U2019t open selection.", @"Message indicating selection couldn't be opened during Open Selection service"),
NSLocalizedString(@"OK", @"OK"), nil, nil);
// No need to report an error string...
}
}
@end
@implementation Controller (ScriptingSupport)
// Scripting support.
- (NSArray *)orderedDocuments {
NSArray *orderedWindows = [NSApp valueForKey:@"orderedWindows"];
unsigned i, c = [orderedWindows count];
NSMutableArray *orderedDocs = [NSMutableArray array];
id curDelegate;
for (i=0; i<c; i++) {
curDelegate = [[orderedWindows objectAtIndex:i] delegate];
if ((curDelegate != nil) && [curDelegate isKindOfClass:[Document class]]) {
[orderedDocs addObject:curDelegate];
}
}
return orderedDocs;
}
- (BOOL)application:(NSApplication *)sender delegateHandlesKey:(NSString *)key {
return [key isEqualToString:@"orderedDocuments"];
}
- (void)insertInOrderedDocuments:(Document *)doc atIndex:(int)index {
[doc retain]; // Keep it around...
[[doc firstTextView] setSelectedRange:NSMakeRange(0, 0)];
[doc setDocumentName:nil];
[doc setDocumentEdited:NO];
[[doc window] makeKeyAndOrderFront:nil];
}
/* Support "make" without the "at" parameter
*/
- (void)insertInOrderedDocuments:(Document *)doc {
[self insertInOrderedDocuments:doc atIndex:0];
}
@end
/* A little function to find the app to open docs of a given extension. Unfortunately there isn't direct NSWorkspace API for this, so we have to create a file and see who would open it. Ugh.
*/
static NSString *fullPathOfAppForType(NSString *type) {
NSString *appName = nil;
NSString *tmpFileName = [NSTemporaryDirectory() stringByAppendingPathComponent:[[[NSProcessInfo processInfo] globallyUniqueString] stringByAppendingString:[@"_TextEdit" stringByAppendingPathExtension:type]]];
if ([[NSFileManager defaultManager] createFileAtPath:tmpFileName contents:[NSData data] attributes:nil]) {
(void)[[NSWorkspace sharedWorkspace] getInfoForFile:tmpFileName application:&appName type:NULL];
(void)[[NSFileManager defaultManager] removeFileAtPath:tmpFileName handler:nil];
}
return appName;
}
Document.h
#import <Cocoa/Cocoa.h>
#import "EncodingManager.h"
@class ScalingScrollView;
/* These get added to the string encodings so we have a common language to refer to file types */
enum {
UnknownStringEncoding = NoStringEncoding,
RichTextStringEncoding = 0xFFFFFFFE,
RichTextWithGraphicsStringEncoding = 0xFFFFFFFD,
HTMLStringEncoding = 0xFFFFFFFC,
SimpleTextStringEncoding = 0xFFFFFFFB,
DocStringEncoding = 0xFFFFFFFA,
WordMLStringEncoding = 0xFFFFFFF9,
WebArchiveStringEncoding = 0xFFFFFFF8,
SmallestCustomStringEncoding = 0xFFFFFFF0
};
typedef enum {
SaveStatusOK = 1,
SaveStatusFileNotWritable, // File is not writable
SaveStatusEncodingNotApplicable, // File can't be converted to specified encoding
SaveStatusDestinationNotWritable, // Destination is not writable
SaveStatusFileEditedExternally, // File was edited externally (by another application)
SaveStatusNotOK = 1000 // Some other error
} SaveStatus;
typedef enum {
FileExtensionHidden = 1,
FileExtensionShown,
FileExtensionPreviousState
} FileExtensionStatus;
/* Returns the default padding on the left/right edges of text views */
float defaultTextPadding(void);
/* Return a non-blank display name. If the display name is blank, currently returns last path component; should probably do better. */
NSString *displayName(NSString *path);
/* Helper used in toggling menu items in validate methods, based on a condition (useFirst) */
void validateToggleItem(NSMenuItem *menuItem, BOOL useFirst, NSString *first, NSString *second);
/* Struct for carrying info for saving between the various routines...
*/
typedef struct _DocumentSaveInfo {
NSString *nameForSaving; // This is retained by this structure
NSString *fileToBeRemoved; // This is retained by this structure
unsigned encodingForSaving;
BOOL haveToChangeType;
BOOL showEncodingAccessory;
BOOL showRichTextDocumentFormatAccessory;
BOOL showRichTextWithGraphicsDocumentFormatAccessory;
BOOL rememberName;
BOOL shouldClose;
BOOL showSavePanel;
BOOL doingRTFDConversion;
SEL whenDoneCallback;
FileExtensionStatus hideExtension;
NSPopUpButton *encodingPopUp;
NSButton *appendPlainTextExtensionButton;
} DocumentSaveInfo;
@interface Document : NSObject {
NSTextStorage *textStorage;
NSString *documentName; /* If nil, never saved */
NSString *revertDocumentName; /* For reverting purposes, if the document is made untitled at some point */
ScalingScrollView *scrollView; /* ScrollView containing document */
NSPrintInfo *printInfo; /* PrintInfo, used when hasMultiplePages is true */
BOOL isDocumentEdited;
BOOL hasMultiplePages;
BOOL isRichText;
BOOL isReadOnly;
BOOL uniqueZone; /* YES if the zone was created specially for this document */
BOOL openedIgnoringRTF; /* Setting at the the time the doc was open (so revert does the same thing) */
BOOL openedIgnoringHTML; /* Setting at the the time the doc was open (so revert does the same thing) */
unsigned documentEncoding; /* NSStringEncoding or one of the above values */
unsigned untitledDocNumber; /* If not 0, the untitled sequence number this document has been assigned */
int changeCount;
BOOL convertedDocument; /* Converted (or filtered) from some other format (and hence not writable) */
BOOL lossyDocument; /* Loaded lossily, so might not be a good idea to overwrite */
BOOL rulerIsBeingDisplayed; /* Indicates that the ruler is being lazily displayed */
NSDate *fileModDate; /* File modification date from the last open or save */
IBOutlet NSView *richTextDocumentFormatAccessory; /* Set when the rich text popup is loaded */
IBOutlet NSPopUpButton *richTextDocumentFormatPopUp; /* Set when the rich text popup is loaded */
// The next six are document properties (applicable only to rich text documents)
NSString *author;
NSString *copyright;
NSString *company;
NSString *title;
NSString *subject;
NSString *comment;
NSString *keywords;
int lastDocumentPropertyChangeCheckpointCount;
NSString *lastChangedDocumentProperty;
}
/* Don't call -(id)init; call one of these methods... */
- (id)initWithPath:(NSString *)filename encoding:(unsigned)encoding uniqueZone:(BOOL)flag error:(NSError **)errorPtr;
- (id)initWithPath:(NSString *)filename encoding:(unsigned)encoding ignoreRTF:(BOOL)ignoreRTF ignoreHTML:(BOOL)ignoreHTML uniqueZone:(BOOL)flag error:(NSError **)errorPtr; /* Should be an absolute path here; nil for untitled. uniqueZone = YES indicates the zone should be recycled when the doc is dealloced. */
+ (id)openDocumentWithPath:(NSString *)filename encoding:(unsigned)encoding behind:(Document *)otherDoc error:(NSError **)errorPtr; /* Brings window front as key, or behind otherDoc if otherDoc is not nil. Checks to see if document already open. */
+ (id)openUntitled:(BOOL)isOpenedAutomatically; /* Brings window front */
/* Put up panels indicating failure to open one or more files. Pass someSucceeded == YES if not known.
*/
+ (void)displayOpenFailures:(NSArray *)errors someSucceeded:(BOOL)someFilesOpened;
/* These set/get the documentName instance var and also set the window title accordingly. "nil" is used if no title. */
- (void)setDocumentName:(NSString *)fileName;
- (NSString *)documentName;
/* These determine if document has been edited since last save */
- (void)setDocumentEdited:(BOOL)flag;
- (BOOL)isDocumentEdited;
/* Is the document rich? */
- (BOOL)isRichText;
- (void)setRichText:(BOOL)flag;
- (void)setRichText:(BOOL)flag dealWithAttachments:(BOOL)attachmentFlag showRuler:(BOOL)rulerFlag;
/* Is the document read-only? */
- (BOOL)isReadOnly;
- (void)setReadOnly:(BOOL)flag;
/* Document background color */
- (NSColor *)backgroundColor;
- (void)setBackgroundColor:(NSColor *)color;
/* Determining whether file has been edited externally */
- (void)setFileModDate:(NSDate *)date;
- (NSDate *)fileModDate;
- (BOOL)isEditedExternally:(NSDate *)newModDateIfKnown;
/* The encoding of the document... */
- (unsigned)encoding;
- (void)setEncoding:(unsigned)encoding;
/* Whether document was converted from some other format (filter services) */
- (BOOL)converted;
- (void)setConverted:(BOOL)flag;
/* Whether document was loaded lossily */
- (BOOL)lossy;
- (void)setLossy:(BOOL)flag;
/* Hyphenation factor (0.0-1.0, 0.0 == disabled) */
- (float)hyphenationFactor;
- (void)setHyphenationFactor:(float)factor;
/* View size (as it should be saved in a RTF file) */
- (NSSize)viewSize;
- (void)setViewSize:(NSSize)size;
/* Ruler management; first call puts up ruler; second call calls the first one in a delayed fashion */
- (void)showRuler:(id)obj;
- (void)showRulerDelayed:(BOOL)flag;
/* Attributes */
- (NSTextStorage *)textStorage;
- (NSTextView *)firstTextView;
- (NSWindow *)window;
- (NSUndoManager *)undoManager;
- (NSLayoutManager *)layoutManager;
/* Misc methods */
- (NSString *)untitledDocumentName:(unsigned)type;
+ (Document *)documentForWindow:(NSWindow *)window;
+ (Document *)documentForPath:(NSString *)filename;
+ (NSString *)cleanedUpPath:(NSString *)filename;
+ (unsigned)numberOfOpenDocuments;
- (void)doForegroundLayoutToCharacterIndex:(unsigned)loc;
- (void)showWindowBehindDocument:(Document *)otherDoc;
- (void)doRevert;
- (int)undoCheckpointCount;
/* Page-oriented methods */
- (void)addPage;
- (void)removePage;
- (unsigned)numberOfPages;
- (void)setHasMultiplePages:(BOOL)flag;
- (void)setHasMultiplePages:(BOOL)flag force:(BOOL)force;
- (BOOL)hasMultiplePages;
- (void)setPrintInfo:(NSPrintInfo *)anObject;
- (NSPrintInfo *)printInfo;
- (void)printInfoUpdated; // To let the document know that printInfo has been changed
- (void)setPaperSize:(NSSize)size;
- (NSSize)paperSize;
/* Printing a document; return value indicates cancel */
- (BOOL)printDocumentModally:(BOOL)modalFlag;
/* Saving helpers. */
- (void)saveDocument:(BOOL)showSavePanel rememberName:(BOOL)rememberNewNameAndSuch shouldClose:(BOOL)shouldClose; /* Entry point for saving w/UI; will show panels and such as necessary */
- (void)saveDocument:(BOOL)showSavePanel name:(NSString *)pathForSaving rememberName:(BOOL)rememberNewNameAndSuch shouldClose:(BOOL)shouldClose whenDone:(SEL)callback; /* Entry point for saving w/UI; will show panels and such as necessary */
- (void)getDocumentNameAndSave:(DocumentSaveInfo *)docInfo;
- (void)doSaveWithName:(DocumentSaveInfo *)docInfo overwriteOK:(BOOL)overwrite;
- (void)askToSave:(SEL)callback;
- (BOOL)canCloseDocument; /* Assures document is saved or user doesn't care about the changes; returns NO if user cancels */
+ (void)openWithEncodingAccessory:(BOOL)flag;
/* Enumerations for saving all edited documents. */
+ (void)saveAllEnumeration:(BOOL)cont;
+ (void)reviewChangesAndQuitEnumeration:(BOOL)cont;
/* Action methods */
+ (void)open:(id)sender;
- (void)saveAs:(id)sender;
- (void)saveTo:(id)sender;
- (void)save:(id)sender;
- (void)revert:(id)sender;
- (void)close:(id)sender;
- (void)doPageLayout:(id)sender;
- (void)toggleRich:(id)sender;
- (void)toggleReadOnly:(id)sender;
- (void)togglePageBreaks:(id)sender;
- (void)printDocument:(id)sender; /* action cover for [self printDocumentUsingPrintPanel:YES] */
- (void)richTextDocumentFormatChanged:(id)sender;
- (void)richTextWithGraphicsDocumentFormatChanged:(id)sender;
/* When the preference "OpenPanelFollowsMainWindow" is set to YES, this is used to get the directory of document in the main window. */
+ (NSString *)directoryOfMainWindow;
/* Delegation messages */
- (void)textView:(NSTextView *)view doubleClickedOnCell:(id <NSTextAttachmentCell>)cell inRect:(NSRect)rect;
- (NSArray *)textView:(NSTextView *)view writablePasteboardTypesForCell:(id <NSTextAttachmentCell>)cell atIndex:(unsigned)charIndex;
- (BOOL)textView:(NSTextView *)view writeCell:(id <NSTextAttachmentCell>)cell atIndex:(unsigned)charIndex toPasteboard:(NSPasteboard *)pboard type:(NSString *)type;
- (void)layoutManager:(NSLayoutManager *)layoutManager didCompleteLayoutForTextContainer:(NSTextContainer *)textContainer atEnd:(BOOL)layoutFinishedFlag;
- (BOOL)windowShouldClose:(id)sender;
- (void)windowWillClose:(NSNotification *)notification;
- (void)undoManagerChangeDone:(NSNotification *)notification;
- (void)undoManagerCheckpoint:(NSNotification *)notification;
- (void)undoManagerChangeUndone:(NSNotification *)notification;
/* Document properties */
- (NSDictionary *)documentPropertyToAttributeNameMappings;
- (NSArray *)knownDocumentProperties;
- (void)clearDocumentProperties;
- (void)setDocumentPropertiesToDefaults;
- (BOOL)hasDocumentProperties;
@end
@interface Document (ReadWrite)
/* File loading. Returns NO if not successful. Doesn't set documentName. */
- (BOOL)loadFromPath:(NSString *)fileName encoding:(unsigned)encoding ignoreRTF:(BOOL)ignoreRTF ignoreHTML:(BOOL)ignoreHTML error:(NSError **)errorPtr; /* If encoding is Unknown, tries to guess */
- (SaveStatus)saveToPath:(NSString *)fileName encoding:(unsigned)encoding updateFilenames:(BOOL)updateFileNamesFlag overwriteOK:(BOOL)overwrite hideExtension:(FileExtensionStatus)extensionStatus;
@end
Document.m
/*
Document.m
Copyright (c) 1995-2005 by Apple Computer, Inc., all rights reserved.
Author: Ali Ozer
Document object for TextEdit.
Needs to be switched over to the NSDocument object.
*/
#import <Cocoa/Cocoa.h>
#import <math.h>
#import <stdio.h> // for NULL
#import <objc/objc-runtime.h> // for objc_msgSend
#import "Document.h"
#import "MultiplePageView.h"
#import "Preferences.h"
#import "EncodingManager.h"
#import "ScalingScrollView.h"
// Functions implemented later in this file
static int nextAvailableUntitledDocNumber(void);
static Document *transientDocument = nil;
static NSRect transientDocumentWindowFrame;
// We have disabled use of per-document zones, but have a way to reenable them in case we need to do per-document memory analysis
static BOOL useZones = NO;
static NSZone *zoneForNewDocument(void) {
return useZones ? NSCreateZone(NSPageSize(), NSPageSize(), YES) : NSDefaultMallocZone();
}
/* A very simple container class which is used to collect the outlets from loading the encoding accessory. No implementation provided, because all of the references are weak and don't need retain/release. Would be nice to be able to switch to a mutable dictionary here at some point.
*/
@interface OpenSaveAccessoryOwner : NSObject {
@public
IBOutlet NSView *accessoryView;
IBOutlet EncodingPopUpButton *encodingPopUp;
IBOutlet NSButton *checkBox;
}
@end
@implementation OpenSaveAccessoryOwner
@end
@implementation Document
- (void)setupInitialTextViewSharedState {
NSTextView *textView = [self firstTextView];
[textView setUsesFontPanel:YES];
[textView setUsesFindPanel:YES];
[textView setDelegate:self];
[textView setAllowsUndo:YES];
[textView setAllowsDocumentBackgroundColorChange:YES];
[textView setContinuousSpellCheckingEnabled:[[Preferences objectForKey:CheckSpellingAsYouType] boolValue]];
[self setRichText:[[Preferences objectForKey:RichText] boolValue] dealWithAttachments:NO showRuler:NO];
if ([self isRichText]) [self showRulerDelayed:YES]; // Bring up the ruler delayed to speed up launch a bit
[self setHyphenationFactor:0.0];
}
- (id)init {
static NSPoint cascadePoint = {0.0, 0.0};
NSLayoutManager *layoutManager;
NSZone *zone = [self zone];
self = [super init];
textStorage = [[NSTextStorage allocWithZone:zone] init];
if (![NSBundle loadNibNamed:@"DocumentWindow" owner:self]) {
NSLog(@"Failed to load DocumentWindow.nib");
[self release];
return nil;
}
layoutManager = [[NSLayoutManager allocWithZone:zone] init];
[textStorage addLayoutManager:layoutManager];
[layoutManager setDelegate:self];
[layoutManager release];
[self setEncoding:UnknownStringEncoding];
// This gives us our first view
[self setHasMultiplePages:[[Preferences objectForKey:ShowPageBreaks] boolValue] force:YES];
// This ensures the first view gets set up correctly
[self setupInitialTextViewSharedState];
[[self window] setDelegate:self];
// Set the window size from defaults...
if ([self hasMultiplePages]) {
[self setViewSize:[[scrollView documentView] pageRectForPageNumber:0].size];
} else {
int windowHeight = [[Preferences objectForKey:WindowHeight] intValue];
int windowWidth = [[Preferences objectForKey:WindowWidth] intValue];
NSFont *font = [Preferences objectForKey:[self isRichText] ? RichTextFont : PlainTextFont];
NSSize size;
size.height = ceil([layoutManager defaultLineHeightForFont:font] * windowHeight);
size.width = [font widthOfString:@"x"]; /* will be 0 if can't be rendered */
if (size.width == 0.0) size.width = [font widthOfString:@" "]; /* try for space width */
if (size.width == 0.0) size.width = [font maximumAdvancement].width; /* or max width */
size.width = ceil(size.width * windowWidth);
[self setViewSize:size];
}
if (NSEqualPoints(cascadePoint, NSZeroPoint)) { /* First time through... */
NSRect frame = [[self window] frame];
cascadePoint = NSMakePoint(frame.origin.x, NSMaxY(frame));
}
cascadePoint = [[self window] cascadeTopLeftFromPoint:cascadePoint];
// Register for appropriate notifications from this window's undo manager to control the state of the close box
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(undoManagerChangeUndone:) name:NSUndoManagerDidUndoChangeNotification object:[self undoManager]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(undoManagerChangeDone:) name:NSUndoManagerDidRedoChangeNotification object:[self undoManager]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(undoManagerChangeDone:) name:NSUndoManagerWillCloseUndoGroupNotification object:[self undoManager]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(undoManagerCheckpoint:) name:NSUndoManagerCheckpointNotification object:[self undoManager]];
return self;
}
- (id)initWithPath:(NSString *)filename encoding:(unsigned)encoding ignoreRTF:(BOOL)ignoreRTF ignoreHTML:(BOOL)ignoreHTML uniqueZone:(BOOL)flag error:(NSError **)errorPtr {
if (!(self = [self init])) {
if (flag) NSRecycleZone([self zone]);
// Return a generic read error; this is a very unlikely error case
if (errorPtr) *errorPtr = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:filename, NSFilePathErrorKey, nil]];
return nil;
}
uniqueZone = flag; /* So if something goes wrong we can recycle the zone correctly in dealloc */
[self setDocumentName:filename];
if (filename) {
if (![self loadFromPath:filename encoding:encoding ignoreRTF:ignoreRTF ignoreHTML:ignoreHTML error:errorPtr]) {
// Cancel showing the ruler - this removes the instance from the runloop and allows it to be released sooner
[self showRulerDelayed:NO];
[self release];
return nil;
}
[[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:[NSURL fileURLWithPath:documentName]];
openedIgnoringRTF = ignoreRTF;
openedIgnoringHTML = ignoreHTML;
if ([self isRichText]) {
if ([self isReadOnly]) {
[self showRulerDelayed:NO]; // Cancel the ruler; no need to show a ruler for readonly docs
} else {
[self showRuler:nil]; // Show the ruler immediately (doing it delayed causes a glitch...)
}
}
}
[[self firstTextView] setSelectedRange:NSMakeRange(0, 0)];
[self setDocumentEdited:NO];
[[self undoManager] removeAllActions];
return self;
}
/* Calls the above routine with the default values for ignoreRTF and ignoreHTML
*/
- (id)initWithPath:(NSString *)filename encoding:(unsigned)encoding uniqueZone:(BOOL)flag error:(NSError **)errorPtr {
BOOL ignoreRTF = [[Preferences objectForKey:IgnoreRichText] boolValue];
BOOL ignoreHTML = [[Preferences objectForKey:IgnoreHTML] boolValue];
return [self initWithPath:filename encoding:encoding ignoreRTF:ignoreRTF ignoreHTML:ignoreHTML uniqueZone:flag error:errorPtr];
}
/* Opens a new, untitled document. The argument specifies whether the document is created automatically by the system (as opposed to by the user).
*/
+ (id)openUntitled:(BOOL)isOpenedAutomatically {
Document *document = [[self allocWithZone:zoneForNewDocument()] initWithPath:nil encoding:UnknownStringEncoding uniqueZone:useZones error:NULL];
if (document) {
transientDocument = nil; // Opening a new untitled does not close previous...
[document setDocumentName:nil];
[document showWindowBehindDocument:nil];
if (isOpenedAutomatically) {
transientDocument = document;
transientDocumentWindowFrame = [[document window] frame];
}
return document;
} else {
return nil;
}
}
+ (id)openDocumentWithPath:(NSString *)filename encoding:(unsigned)encoding ignoreRTF:(BOOL)ignoreRTF ignoreHTML:(BOOL)ignoreHTML behind:(Document *)otherDoc error:(NSError **)errorPtr {
Document *document = [self documentForPath:filename];
if (document && [document isEditedExternally:nil]) { // If the document is already open, but has been edited externally
if ([document isDocumentEdited]) { // and has also been edited in TextEdit, do the conservative thing and don't open
NSBeginAlertSheet(NSLocalizedString(@"Couldn\\U2019t reopen file", @"Title of alert indicating file couldn't be reloaded"),
NSLocalizedString(@"OK", @"OK"),
nil, nil, [document window], self, NULL, NULL, document,
NSLocalizedString(@"The file you are trying to open, \\U201c%@\\U201d, is already open, has unsaved changes, and has been changed by another application. Because opening the file would cause you to lose your changes, request to open the file has been ignored.", @"Message indicating document couldn't be opened because doing so would cause changes to be lost."),
displayName([document documentName]));
return document;
} else { // If the document in TextEdit doesn't have any edits, then simply do a revert
[document doRevert];
}
}
if (!document) {
document = [[self allocWithZone:zoneForNewDocument()] initWithPath:filename encoding:encoding ignoreRTF:ignoreRTF ignoreHTML:ignoreHTML uniqueZone:useZones error:errorPtr];
if (!document) return nil;
}
[document showWindowBehindDocument:otherDoc];
// Do foreground layout only for the front-most document; others can be laid out in the background
if (!otherDoc) [document doForegroundLayoutToCharacterIndex:[[Preferences objectForKey:ForegroundLayoutToIndex] intValue]];
return document;
}
+ (id)openDocumentWithPath:(NSString *)filename encoding:(unsigned)encoding behind:(Document *)otherDoc error:(NSError **)errorPtr {
BOOL ignoreRTF = [[Preferences objectForKey:IgnoreRichText] boolValue];
BOOL ignoreHTML = [[Preferences objectForKey:IgnoreHTML] boolValue];
return [self openDocumentWithPath:filename encoding:encoding ignoreRTF:ignoreRTF ignoreHTML:ignoreHTML behind:otherDoc error:errorPtr];
}
/* Clear the delegates of the text views and window, then release all resources and go away...
*/
- (void)dealloc {
// Remove this document from the notification center as the observer of any notifications...
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (rulerIsBeingDisplayed) [self showRulerDelayed:NO]; // This cancels outstanding request to show ruler...
[[self firstTextView] setDelegate:nil];
[[self layoutManager] setDelegate:nil];
[[self window] setDelegate:nil];
[richTextDocumentFormatAccessory release];
[documentName release];
[revertDocumentName release];
[fileModDate release];
[textStorage release];
[printInfo release];
[author release];
[comment release];
[subject release];
[title release];
[keywords release];
[copyright release];
[lastChangedDocumentProperty release];
if (uniqueZone) NSRecycleZone([self zone]);
if (transientDocument == self) transientDocument = nil;
[super dealloc];
}
- (NSDictionary *)defaultTextAttributes:(BOOL)forRichText {
static NSParagraphStyle *defaultRichParaStyle = nil;
NSMutableDictionary *textAttributes = [[[NSMutableDictionary alloc] initWithCapacity:2] autorelease];
if (isRichText) {
[textAttributes setObject:[Preferences objectForKey:RichTextFont] forKey:NSFontAttributeName];
if (defaultRichParaStyle == nil) { // We do this once...
int cnt;
NSString *measurementUnits = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleMeasurementUnits"];
float tabInterval = ([@"Centimeters" isEqual:measurementUnits]) ? (72.0 / 2.54) : (72.0 / 2.0); // Every cm or half inch
NSMutableParagraphStyle *paraStyle = [[NSMutableParagraphStyle alloc] init];
[paraStyle setTabStops:[NSArray array]]; // This first clears all tab stops
for (cnt = 0; cnt < 12; cnt++) { // Add 12 tab stops, at desired intervals...
NSTextTab *tabStop = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:tabInterval * (cnt + 1)];
[paraStyle addTabStop:tabStop];
[tabStop release];
}
defaultRichParaStyle = [paraStyle copy];
[paraStyle release];
}
[textAttributes setObject:defaultRichParaStyle forKey:NSParagraphStyleAttributeName];
} else {
NSFont *plainFont = [Preferences objectForKey:PlainTextFont];
unsigned tabWidth = [[Preferences objectForKey:TabWidth] intValue];
float charWidth;
if ([plainFont glyphIsEncoded:(NSGlyph)' ']) {
charWidth = [[plainFont screenFontWithRenderingMode:NSFontDefaultRenderingMode] advancementForGlyph:(NSGlyph)' '].width;
} else {
charWidth = [[plainFont screenFontWithRenderingMode:NSFontDefaultRenderingMode] maximumAdvancement].width;
}
// Now use a default paragraph style, but with the tab width adjusted
NSMutableParagraphStyle *mStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[mStyle setTabStops:[NSArray array]];
[mStyle setDefaultTabInterval:(charWidth * tabWidth)];
NSParagraphStyle *style = [mStyle copy];
[textAttributes setObject:style forKey:NSParagraphStyleAttributeName];
[mStyle release];
[style release];
// Also set the font
[textAttributes setObject:plainFont forKey:NSFontAttributeName];
}
return textAttributes;
}
- (NSTextView *)firstTextView {
return [[self layoutManager] firstTextView];
}
/* We take "viewSize" to mean the pure size of the text area, so margins or line fragment paddings don't count. The ruler ends up being included though.
*/
- (NSSize)viewSize {
NSSize size = [NSScrollView contentSizeForFrameSize:[scrollView frame].size hasHorizontalScroller:[scrollView hasHorizontalScroller] hasVerticalScroller:[scrollView hasVerticalScroller] borderType:[scrollView borderType]];
if (![self hasMultiplePages]) {
size.width -= (defaultTextPadding() * 2.0);
}
return size;
}
- (void)setViewSize:(NSSize)size {
NSWindow *window = [scrollView window];
NSRect origWindowFrame = [window frame];
NSSize scrollViewSize;
if (![self hasMultiplePages]) {
size.width += (defaultTextPadding() * 2.0);
}
scrollViewSize = [NSScrollView frameSizeForContentSize:size hasHorizontalScroller:[scrollView hasHorizontalScroller] hasVerticalScroller:[scrollView hasVerticalScroller] borderType:[scrollView borderType]];
[window setContentSize:scrollViewSize];
[window setFrameTopLeftPoint:NSMakePoint(origWindowFrame.origin.x, NSMaxY(origWindowFrame))];
}
/* Show the window for the document, but also replace the transient untitled document if there's one. If otherDoc is provided, bring up document behind this other document. Otherwise come to front, as key.
*/
- (void)showWindowBehindDocument:(Document *)otherDoc {
BOOL closeTransient = transientDocument && NSEqualRects(transientDocumentWindowFrame, [[transientDocument window] frame]);
if (closeTransient) {
[[self window] setFrameTopLeftPoint:NSMakePoint(transientDocumentWindowFrame.origin.x, NSMaxY(transientDocumentWindowFrame))];
}
int otherWindowNumber = otherDoc ? [[otherDoc window] windowNumber] : 0;
if (otherWindowNumber) {
[[self window] orderWindow:NSWindowBelow relativeTo:otherWindowNumber];
} else {
[[self window] makeKeyAndOrderFront:nil];
}
if (closeTransient) { // Should be ready to close, unedited
[transientDocument close:nil];
}
}
/* This method causes the text to be laid out in the foreground (approximately) up to the indicated character index.
*/
- (void)doForegroundLayoutToCharacterIndex:(unsigned)loc {
unsigned len;
if (loc > 0 && (len = [[self textStorage] length]) > 0) {
NSRange glyphRange;
if (loc >= len) loc = len - 1;
/* Find out which glyph index the desired character index corresponds to */
glyphRange = [[self layoutManager] glyphRangeForCharacterRange:NSMakeRange(loc, 1) actualCharacterRange:NULL];
if (glyphRange.location > 0) {
/* Now cause layout by asking a question which has to determine where the glyph is */
(void)[[self layoutManager] textContainerForGlyphAtIndex:glyphRange.location - 1 effectiveRange:NULL];
}
}
}
+ (NSString *)cleanedUpPath:(NSString *)filename {
NSString *resolvedSymlinks = [filename stringByResolvingSymlinksInPath];
if ([resolvedSymlinks length] > 0) {
NSString *standardized = [resolvedSymlinks stringByStandardizingPath];
return [standardized length] ? standardized : resolvedSymlinks;
}
return filename;
}
// newModDateIfKnown is optional (as a perf optimization); if not available, obtain it for the existing document name
//
- (BOOL)isEditedExternally:(NSDate *)newModDateIfKnown {
NSDate *curModDate = [self fileModDate];
if (!curModDate) return NO; // Not much we can do if we don't know anything about the file from before
if (!newModDateIfKnown && !(newModDateIfKnown = [[[NSFileManager defaultManager] fileAttributesAtPath:[self documentName] traverseLink:YES] fileModificationDate])) return NO;
return [curModDate isEqual:newModDateIfKnown] ? NO : YES;
}
- (void)setFileModDate:(NSDate *)date {
if (date != fileModDate) {
[fileModDate release];
fileModDate = [date copy];
}
}
- (NSDate *)fileModDate {
return [[fileModDate retain] autorelease];
}
- (void)setDocumentName:(NSString *)filename updateIcon:(BOOL)updateIcon {
if (filename) {
BOOL same = [filename isEqual:documentName];
[documentName autorelease];
[revertDocumentName autorelease]; // Name the document will use to revert to (having a separate ivar allows docs whose formats have changed to revert)
documentName = [[[self class] cleanedUpPath:filename] copyWithZone:[self zone]];
revertDocumentName = [documentName copyWithZone:[self zone]];
if (same && updateIcon) [[self window] setTitleWithRepresentedFilename:@""]; // Workaround NSWindow optimization
[[self window] setTitleWithRepresentedFilename:documentName];
if (uniqueZone) NSSetZoneName([self zone], documentName);
} else {
NSString *untitled = [self untitledDocumentName:UnknownStringEncoding];
[[self window] setTitle:untitled];
if (uniqueZone) NSSetZoneName([self zone], untitled);
documentName = nil;
}
}
- (void)setDocumentName:(NSString *)filename {
[self setDocumentName:filename updateIcon:NO];
}
- (NSString *)documentName {
return documentName;
}
// Note that because setDocumentEdited: is called from undo notifications, undoable actions do not need to concern themselves with calling this.
//
- (void)setDocumentEdited:(BOOL)flag {
if (flag != isDocumentEdited) {
isDocumentEdited = flag;
[[self window] setDocumentEdited:isDocumentEdited];
if (transientDocument == self) transientDocument = nil;
}
if (!isDocumentEdited) changeCount = 0;
}
- (BOOL)isDocumentEdited {
return isDocumentEdited;
}
- (void)setReadOnly:(BOOL)flag {
isReadOnly = flag;
[[self firstTextView] setEditable:!isReadOnly];
}
- (BOOL)isReadOnly {
return isReadOnly;
}
- (void)setBackgroundColor:(NSColor *)color {
[[self firstTextView] setBackgroundColor:color];
}
- (NSColor *)backgroundColor {
return [[self firstTextView] backgroundColor];
}
- (NSTextStorage *)textStorage {
return textStorage;
}
- (NSWindow *)window {
return [[self firstTextView] window];
}
- (BOOL)hasSheet {
return [[self window] attachedSheet] ? YES : NO;
}
- (NSUndoManager *)undoManager {
return [[self window] undoManager];
}
- (NSLayoutManager *)layoutManager {
return [[textStorage layoutManagers] objectAtIndex:0];
}
- (void)printInfoUpdated {
if ([self hasMultiplePages]) {
unsigned cnt, numberOfPages = [self numberOfPages];
MultiplePageView *pagesView = [scrollView documentView];
NSArray *textContainers = [[self layoutManager] textContainers];
[pagesView setPrintInfo:printInfo];
for (cnt = 0; cnt < numberOfPages; cnt++) {
NSRect textFrame = [pagesView documentRectForPageNumber:cnt];
NSTextContainer *textContainer = [textContainers objectAtIndex:cnt];
[textContainer setContainerSize:textFrame.size];
[[textContainer textView] setFrame:textFrame];
}
}
}
- (void)setPrintInfo:(NSPrintInfo *)anObject {
if (printInfo == anObject) return;
[printInfo autorelease];
printInfo = [anObject copyWithZone:[self zone]];
[self printInfoUpdated];
}
// Create and return the printInfo lazily
- (NSPrintInfo *)printInfo {
if (printInfo == nil) {
[self setPrintInfo:[NSPrintInfo sharedPrintInfo]];
[printInfo setHorizontalPagination:NSFitPagination];
[printInfo setHorizontallyCentered:NO];
[printInfo setVerticallyCentered:NO];
[printInfo setLeftMargin:72.0];
[printInfo setRightMargin:72.0];
[printInfo setTopMargin:72.0];
[printInfo setBottomMargin:72.0];
}
return printInfo;
}
- (NSSize)paperSize {
return [[self printInfo] paperSize];
}
- (void)setPaperSize:(NSSize)size {
[[self printInfo] setPaperSize:size];
[self printInfoUpdated];
}
/* Multiple page related code */
- (unsigned)numberOfPages {
return hasMultiplePages ? [[scrollView documentView] numberOfPages] : 1;
}
- (BOOL)hasMultiplePages {
return hasMultiplePages;
}
- (void)addPage {
NSZone *zone = [self zone];
unsigned numberOfPages = [self numberOfPages];
MultiplePageView *pagesView = [scrollView documentView];
NSSize textSize = [pagesView documentSizeInPage];
NSTextContainer *textContainer = [[NSTextContainer allocWithZone:zone] initWithContainerSize:textSize];
NSTextView *textView;
[pagesView setNumberOfPages:numberOfPages + 1];
textView = [[NSTextView allocWithZone:zone] initWithFrame:[pagesView documentRectForPageNumber:numberOfPages] textContainer:textContainer];
[textView setHorizontallyResizable:NO];
[textView setVerticallyResizable:NO];
[pagesView addSubview:textView];
[[self layoutManager] addTextContainer:textContainer];
[textView release];
[textContainer release];
}
- (void)removePage {
unsigned numberOfPages = [self numberOfPages];
NSArray *textContainers = [[self layoutManager] textContainers];
NSTextContainer *lastContainer = [textContainers objectAtIndex:[textContainers count] - 1];
MultiplePageView *pagesView = [scrollView documentView];
[pagesView setNumberOfPages:numberOfPages - 1];
[[lastContainer textView] removeFromSuperview];
[[lastContainer layoutManager] removeTextContainerAtIndex:[textContainers count] - 1];
}
/* This method sets whether the document has multiple pages or not. It can be called at anytime. force indicates whether to do the task even if the values are the same (usually needed for the first call).
*/
- (void)setHasMultiplePages:(BOOL)flag force:(BOOL)force {
NSZone *zone = [self zone];
if (!force && (hasMultiplePages == flag)) return;
hasMultiplePages = flag;
if (hasMultiplePages) {
NSTextView *textView = [self firstTextView];
MultiplePageView *pagesView = [[MultiplePageView allocWithZone:zone] init];
[scrollView setDocumentView:pagesView];
[pagesView setPrintInfo:[self printInfo]];
// Add the first new page before we remove the old container so we can avoid losing all the shared text view state.
[self addPage];
if (textView) {
[[self layoutManager] removeTextContainerAtIndex:0];
}
[scrollView setHasHorizontalScroller:YES];
// Make sure the selected text is shown
[[self firstTextView] scrollRangeToVisible:[[self firstTextView] selectedRange]];
NSRect visRect = [pagesView visibleRect];
NSRect pageRect = [pagesView pageRectForPageNumber:0];
if (visRect.size.width < pageRect.size.width) { // If we can't show the whole page, tweak a little further
NSRect docRect = [pagesView documentRectForPageNumber:0];
if (visRect.size.width >= docRect.size.width) { // Center document area in window
visRect.origin.x = docRect.origin.x - floor((visRect.size.width - docRect.size.width) / 2);
if (visRect.origin.x < pageRect.origin.x) visRect.origin.x = pageRect.origin.x;
} else { // If we can't show the document area, then show left edge of document area (w/out margins)
visRect.origin.x = docRect.origin.x;
}
[pagesView scrollRectToVisible:visRect];
}
} else {
NSSize size = [scrollView contentSize];
NSTextContainer *textContainer = [[NSTextContainer allocWithZone:zone] initWithContainerSize:NSMakeSize(size.width, FLT_MAX)];
NSTextView *textView = [[NSTextView allocWithZone:zone] initWithFrame:NSMakeRect(0.0, 0.0, size.width, size.height) textContainer:textContainer];
// Insert the single container as the first container in the layout manager before removing the existing pages in order to preserve the shared view state.
[[self layoutManager] insertTextContainer:textContainer atIndex:0];
if ([[scrollView documentView] isKindOfClass:[MultiplePageView class]]) {
NSArray *textContainers = [[self layoutManager] textContainers];
unsigned cnt = [textContainers count];
while (cnt-- > 1) {
[[self layoutManager] removeTextContainerAtIndex:cnt];
}
}
[textContainer setWidthTracksTextView:YES];
[textContainer setHeightTracksTextView:NO]; /* Not really necessary */
[textView setHorizontallyResizable:NO]; /* Not really necessary */
[textView setVerticallyResizable:YES];
[textView setAutoresizingMask:NSViewWidthSizable];
[textView setMinSize:size]; /* Not really necessary; will be adjusted by the autoresizing... */
[textView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; /* Will be adjusted by the autoresizing... */
/* The next line should cause the multiple page view and everything else to go away */
[scrollView setDocumentView:textView];
[scrollView setHasHorizontalScroller:NO];
[textView release];
[textContainer release];
// Show the selected region
[[self firstTextView] scrollRangeToVisible:[[self firstTextView] selectedRange]];
}
[[scrollView window] makeFirstResponder:[self firstTextView]];
[[scrollView window] setInitialFirstResponder:[self firstTextView]]; // So focus won't be stolen (2934918)
}
- (void)setHasMultiplePages:(BOOL)flag {
[self setHasMultiplePages:flag force:NO];
}
/* Used when converting to plain text
*/
- (void)removeAttachments {
NSTextStorage *attrString = [self textStorage];
NSTextView *view = [self firstTextView];
unsigned loc = 0;
unsigned end = [attrString length];
[attrString beginEditing];
while (loc < end) { /* Run through the string in terms of attachment runs */
NSRange attachmentRange; /* Attachment attribute run */
NSTextAttachment *attachment = [attrString attribute:NSAttachmentAttributeName atIndex:loc longestEffectiveRange:&attachmentRange inRange:NSMakeRange(loc, end-loc)];
if (attachment != nil) { /* If there is an attachment, make sure it is valid */
unichar ch = [[attrString string] characterAtIndex:loc];
if (ch == NSAttachmentCharacter) {
if ([view shouldChangeTextInRange:NSMakeRange(loc, 1) replacementString:@""]) {
[attrString replaceCharactersInRange:NSMakeRange(loc, 1) withString:@""];
[view didChangeText];
}
end = [attrString length]; /* New length */
} else {
loc++; /* Just skip over the current character... */
}
} else {
loc = NSMaxRange(attachmentRange);
}
}
[attrString endEditing];
}
/* Hyphenation related methods.
*/
- (void)setHyphenationFactor:(float)factor {
[[self layoutManager] setHyphenationFactor:factor];
}
- (float)hyphenationFactor {
return [[self layoutManager] hyphenationFactor];
}
/* Encoding...
*/
- (unsigned)encoding {
return documentEncoding;
}
- (void)setEncoding:(unsigned)encoding {
documentEncoding = encoding;
}
- (BOOL)converted {
return convertedDocument;
}
- (void)setConverted:(BOOL)flag {
convertedDocument = flag;
}
- (BOOL)lossy {
return lossyDocument;
}
- (void)setLossy:(BOOL)flag {
lossyDocument = flag;
}
/* Method to lazily display ruler. Call with YES to display, NO to cancel display; this method doesn't remove the ruler.
*/
- (void)showRulerDelayed:(BOOL)flag {
if (!flag && rulerIsBeingDisplayed) {
[[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(showRuler:) object:self];
} else if (flag && !rulerIsBeingDisplayed) {
[self performSelector:@selector(showRuler:) withObject:self afterDelay:0.0];
}
rulerIsBeingDisplayed = flag;
}
- (void)showRuler:(id)obj {
if (rulerIsBeingDisplayed && !obj) [self showRulerDelayed:NO]; // Cancel outstanding request, if not coming from the delayed request
if ([[Preferences objectForKey:ShowRuler] boolValue]) [[self firstTextView] setRulerVisible:YES];
}
/* Doesn't check to see if the prev value is the same --- Otherwise the first time doesn't work...
attachmentFlag allows for optimizing some cases where we know we have no attachments, so we don't need to scan looking for them.
*/
- (void)setRichText:(BOOL)flag dealWithAttachments:(BOOL)attachmentFlag showRuler:(BOOL)rulerFlag {
NSTextView *view = [self firstTextView];
NSDictionary *textAttributes;
NSParagraphStyle *paragraphStyle;
isRichText = flag;
[view setRichText:isRichText];
[view setUsesRuler:isRichText]; // If NO, this correctly gets rid of the ruler if it was up
if (!isRichText && rulerIsBeingDisplayed) [self showRulerDelayed:NO]; // Cancel delayed ruler request
if (isRichText && rulerFlag) [self showRuler:nil];
[view setImportsGraphics:isRichText];
textAttributes = [self defaultTextAttributes:isRichText];
paragraphStyle = [textAttributes objectForKey:NSParagraphStyleAttributeName];
// Note, since the textview content changes (removing attachments and changing attributes) create undo actions inside the textview, we do not execute them here if we're undoing or redoing
if (![[self undoManager] isUndoing] && ![[self undoManager] isRedoing]) {
if (!isRichText && attachmentFlag) [self removeAttachments];
NSRange range = NSMakeRange(0, [textStorage length]);
if ([view shouldChangeTextInRange:range replacementString:nil]) {
[textStorage setAttributes:textAttributes range: range];
[view didChangeText];
}
}
[view setTypingAttributes:textAttributes];
[view setDefaultParagraphStyle:paragraphStyle];
if (!isRichText) {
[self clearDocumentProperties];
} else {
[self setDocumentPropertiesToDefaults];
}
}
- (void)setRichText:(BOOL)flag {
[self setRichText:flag dealWithAttachments:YES showRuler:YES];
}
- (BOOL)isRichText {
return isRichText;
}
/* Document properties management */
/* Table mapping document property keys "company", etc, to text system document attribute keys (NSCompanyDocumentAttribute, etc)
*/
- (NSDictionary *)documentPropertyToAttributeNameMappings {
static NSDictionary *dict = nil;
if (!dict) dict = [[NSDictionary alloc] initWithObjectsAndKeys:
NSCompanyDocumentAttribute, @"company",
NSAuthorDocumentAttribute, @"author",
NSKeywordsDocumentAttribute, @"keywords",
NSCopyrightDocumentAttribute, @"copyright",
NSTitleDocumentAttribute, @"title",
NSSubjectDocumentAttribute, @"subject",
NSCommentDocumentAttribute, @"comment", nil];
return dict;
}
- (NSArray *)knownDocumentProperties {
return [[self documentPropertyToAttributeNameMappings] allKeys];
}
/* If there are document properties and they are not the same as the defaults established in preferences, return YES
*/
- (BOOL)hasDocumentProperties {
NSEnumerator *keyEnumerator = [[self knownDocumentProperties] objectEnumerator];
NSString *key;
while (key = [keyEnumerator nextObject]) {
id value = [self valueForKey:key];
if (value && ![value isEqual:[Preferences objectForKey:key]]) return YES;
}
return NO;
}
/* This actually clears all properties (rather than setting them to default values established in preferences)
*/
- (void)clearDocumentProperties {
NSEnumerator *keyEnumerator = [[self knownDocumentProperties] objectEnumerator];
NSString *key;
while (key = [keyEnumerator nextObject]) [self setValue:nil forKey:key];
}
/* This sets document properties to values established in defaults
*/
- (void)setDocumentPropertiesToDefaults {
NSEnumerator *keyEnumerator = [[self knownDocumentProperties] objectEnumerator];
NSString *key;
while (key = [keyEnumerator nextObject]) [self setValue:[Preferences objectForKey:key] forKey:key];
}
/* We implement a setValue:forDocumentProperty: to work around NSUndoManager bug where prepareWithInvocationTarget: fails to freeze-dry invocations with "known" methods such as setValue:forKey:.
*/
- (void)setValue:(id)value forDocumentProperty:(NSString *)property {
id oldValue = [self valueForKey:property];
if (value == oldValue || [value isEqual:oldValue]) return; // Workaround for bug 3752890 "Becoming non-editable causes textfield value to be committed instead of discarded"
// We want to do coalesced undo of changes to a single field. Although this would normally be done by undo grouping, another way to approach is to avoid registering unnecessary undo events altogether. We do that here, by checking the "checkpoint count" (which serves as generation count) before registering.
if ((lastDocumentPropertyChangeCheckpointCount != [self undoCheckpointCount]) || ![lastChangedDocumentProperty isEqual:property]) {
lastDocumentPropertyChangeCheckpointCount = [self undoCheckpointCount] + 1;
[lastChangedDocumentProperty release];
lastChangedDocumentProperty = [property copy];
[[[self undoManager] prepareWithInvocationTarget:self] setValue:oldValue forDocumentProperty:property];
[[self undoManager] setActionName:NSLocalizedString(property, "")]; // Potential strings for action names are listed below (for genstrings to pick up)
}
// Then we call the regular KVC mechanism to get the value to be properly set
[super setValue:value forKey:property];
}
- (void)setValue:(id)value forKey:(NSString *)key {
if ([[self knownDocumentProperties] containsObject:key]) {
[self setValue:value forDocumentProperty:key]; // We take a side-trip to this method to register for undo
} else {
[super setValue:value forKey:key]; // In case some other KVC call is sent to Document, we treat it normally
}
}
/* For genstrings:
NSLocalizedStringWithDefaultValue(@"author", @"", @"", @"Change Author", @"Undo menu change string, without the 'Undo'");
NSLocalizedStringWithDefaultValue(@"copyright", @"", @"", @"Change Copyright", @"Undo menu change string, without the 'Undo'");
NSLocalizedStringWithDefaultValue(@"subject", @"", @"", @"Change Subject", @"Undo menu change string, without the 'Undo'");
NSLocalizedStringWithDefaultValue(@"title", @"", @"", @"Change Title", @"Undo menu change string, without the 'Undo'");
NSLocalizedStringWithDefaultValue(@"company", @"", @"", @"Change Company", @"Undo menu change string, without the 'Undo'");
NSLocalizedStringWithDefaultValue(@"comment", @"", @"", @"Change Comment", @"Undo menu change string, without the 'Undo'");
NSLocalizedStringWithDefaultValue(@"keywords", @"", @"", @"Change Keywords", @"Undo menu change string, without the 'Undo'");
*/
/* Outlet methods */
- (void)setScrollView:(id)anObject {
scrollView = anObject;
[scrollView setHasVerticalScroller:YES];
[scrollView setHasHorizontalScroller:NO];
[[scrollView contentView] setAutoresizesSubviews:YES];
[[scrollView contentView] setBackgroundColor:[NSColor controlColor]];
if (NSInterfaceStyleForKey(NSInterfaceStyleDefault, scrollView) == NSWindows95InterfaceStyle) {
[scrollView setBorderType:NSBezelBorder];
}
}
/* User commands... */
- (BOOL)printDocumentModally:(BOOL)modalFlag {
BOOL success = YES;
NSPrintInfo *tempPrintInfo = [self printInfo];
if ([[Preferences objectForKey:NumberPagesWhenPrinting] boolValue]) {
tempPrintInfo = [[tempPrintInfo copy] autorelease];
[[tempPrintInfo dictionary] setValue:[NSNumber numberWithBool:YES] forKey:NSPrintHeaderAndFooter];
}
NSPrintOperation *op = [NSPrintOperation printOperationWithView:[scrollView documentView] printInfo:tempPrintInfo];
[op setShowPanels:YES];
[self doForegroundLayoutToCharacterIndex:INT_MAX]; // Make sure the whole document is laid out before printing
if (modalFlag) {
success = [op runOperation];
} else {
[op runOperationModalForWindow:[self window] delegate:nil didRunSelector:NULL contextInfo:NULL];
}
// The return value is interesting only in the modal case, where cancelling or failure from one printing should cancel the rest.
return success;
}
- (void)printDocument:(id)sender {
[self printDocumentModally:NO];
}
/* Toggles read-only state of the document
*/
- (void)toggleReadOnly:(id)sender {
[[self undoManager] registerUndoWithTarget:self selector:@selector(toggleReadOnly:) object:nil];
[[self undoManager] setActionName:[self isReadOnly] ?
NSLocalizedString(@"Allow Editing", @"Menu item to make the current document editable (not read-only)") :
NSLocalizedString(@"Prevent Editing", @"Menu item to make the current document read-only")];
[self setReadOnly:![self isReadOnly]];
}
/* This is an undoable entry point for switching a document between rich and plain states.
*/
- (void)setDocumentState:(BOOL)rich name:(NSString *)name encoding:(unsigned)enc converted:(BOOL)conv {
[[[self undoManager] prepareWithInvocationTarget:self] setDocumentState:isRichText name:documentName encoding:documentEncoding converted:convertedDocument];
[self setRichText:rich];
[self setEncoding:enc];
[self setConverted:conv];
[self setDocumentName:name];
// We would normally provide a localized undo action name here, but since this change is coming after "localization freeze," we are left with a string which we don't need to localize
[[self undoManager] setActionName:@""];
}
/* doToggleRich, called from toggleRich: or the endToggleRichSheet:... alert panel method, toggles the isRichText state
*/
- (void)doToggleRich {
[self setDocumentState:!isRichText name:nil encoding:UnknownStringEncoding converted:NO];
}
/* toggleRich: puts up an alert before ultimately calling doToggleRich
*/
- (void)toggleRich:(id)sender {
int length = [textStorage length];
NSRange range;
NSDictionary *attrs;
// If we are rich and any of the text attrs have been changed from the default, or there are any doc properties, put up an alert first...
if (isRichText && ((length > 0) && (attrs = [textStorage attributesAtIndex:0 effectiveRange:&range]) && ((attrs == nil) || (range.length < length) || ![[self defaultTextAttributes:YES] isEqual:attrs])) || [self hasDocumentProperties]) {
NSBeginAlertSheet(NSLocalizedString(@"Convert this document to plain text?", @"Title of alert confirming Make Plain Text"),
NSLocalizedString(@"OK", @"OK"), NSLocalizedString(@"Cancel", @"Button choice allowing user to cancel."), nil, [self window],
self, NULL, @selector(didEndToggleRichSheet:returnCode:contextInfo:), NULL,
NSLocalizedString(@"If you convert this document, you will lose all text styles (such as fonts and colors) and document properties.", @"Subtitle of alert confirming Make Plain Text"));
} else {
[self doToggleRich];
}
}
- (void)didEndToggleRichSheet:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
if (returnCode == NSAlertDefaultReturn) [self doToggleRich];
}
- (void)togglePageBreaks:(id)sender {
[self setHasMultiplePages:![self hasMultiplePages]];
}
- (void)toggleHyphenation:(id)sender {
[self setHyphenationFactor:([self hyphenationFactor] > 0.0) ? 0.0 : 0.9]; /* Toggle between 0.0 and 0.9 */
if ([self isRichText]) [self setDocumentEdited:YES];
}
- (void)doPageLayout:(id)sender {
NSPrintInfo *tempPrintInfo = [[self printInfo] copy];
NSPageLayout *pageLayout = [NSPageLayout pageLayout];
[pageLayout beginSheetWithPrintInfo:tempPrintInfo modalForWindow:[self window] delegate:self didEndSelector:@selector(didEndPageLayout:returnCode:contextInfo:) contextInfo:(void *)tempPrintInfo];
}
- (void)didEndPageLayout:(NSPageLayout *)pageLayout returnCode:(int)result contextInfo:(void *)contextInfo {
NSPrintInfo *tempPrintInfo = (NSPrintInfo *)contextInfo;
if (result == NSOKButton) [self setPrintInfo:tempPrintInfo];
[tempPrintInfo release];
}
/* doRevert does the actual reverting...
*/
- (void)doRevert {
NSRange prevSelectedRange = [[self firstTextView] selectedRange];
if (![self loadFromPath:revertDocumentName encoding:documentEncoding ignoreRTF:openedIgnoringRTF ignoreHTML:openedIgnoringHTML error:NULL]) {
NSString *alertTitle = [NSString stringWithFormat:NSLocalizedString(@"Couldn\\U2019t revert to saved version of \\U201c%@\\U201d.", @"Title of alert panel indicating file couldn't be reverted."), displayName(revertDocumentName)];
// No endSheet method required for the alert, as there's only one possible response and nothing to clean up
NSBeginAlertSheet(alertTitle,
NSLocalizedString(@"OK", @"OK"), nil, nil, [self window],
self, NULL, NULL, NULL,
NSLocalizedString(@"The file has been removed.", @"Subtitle of alert panel indicating file couldn't be reverted."));
} else {
// Restore selection, if still within bounds
if (NSMaxRange(prevSelectedRange) <= [textStorage length]) {
[[self firstTextView] setSelectedRange:prevSelectedRange];
} else {
[[self firstTextView] setSelectedRange:NSMakeRange(0, 0)];
}
[self setDocumentName:revertDocumentName];
[[self undoManager] removeAllActions];
[self setDocumentEdited:NO];
[[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:[NSURL fileURLWithPath:revertDocumentName]];
}
}
/* revert: puts up an alert or calls doRevert directly
*/
- (void)revert:(id)sender {
if (revertDocumentName != nil) {
if ([self isDocumentEdited]) {
NSString *alertTitle = [NSString stringWithFormat:NSLocalizedString(@"Revert to saved version of \\U201c%@\\U201d?", @"Title of alert panel warning user about effects of reverting."), displayName(revertDocumentName)];
NSBeginAlertSheet(alertTitle,
NSLocalizedString(@"OK", @"OK"), NSLocalizedString(@"Cancel", @"Button choice allowing user to cancel."), nil, [self window], self, NULL, @selector(didEndRevertSheet:returnCode:contextInfo:), NULL,
NSLocalizedString(@"Reverting will lose your current changes.", @"Subtitle of alert panel warning user about effects of reverting."));
} else {
[self doRevert];
}
}
}
- (void)didEndRevertSheet:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
if (returnCode == NSAlertDefaultReturn) [self doRevert];
}
- (void)close:(id)sender {
[[self window] close];
}
/* This no longer has a menu item ("Save To...") by default. It basically allows saving the file elsewhere w/out changing where it is.
*/
- (void)saveTo:(id)sender {
[self saveDocument:YES rememberName:NO shouldClose:NO];
}
- (void)saveAs:(id)sender {
[self saveDocument:YES rememberName:YES shouldClose:NO];
}
- (void)save:(id)sender {
[self saveDocument:NO rememberName:YES shouldClose:NO];
}
/* Used for chaining saves in cases where all edited documents need to be saved. When one document is saved, it calls this method with the flag argument indicating whether the save was cancelled or not.
*/
+ (void)saveAllEnumeration:(BOOL)cont {
if (cont) {
NSArray *windows = [NSApp windows];
unsigned count = [windows count];
while (count--) {
NSWindow *window = [windows objectAtIndex:count];
Document *document = [Document documentForWindow:window];
if (document) {
if ([document isDocumentEdited]) {
[document saveDocument:NO name:nil rememberName:YES shouldClose:NO whenDone:@selector(saveAllEnumeration:)];
return;
}
}
}
}
}
/* Same as above, but once there are no more documents to save, quits the app.
*/
+ (void)reviewChangesAndQuitEnumeration:(BOOL)cont {
if (cont) {
NSArray *windows = [NSApp windows];
unsigned count = [windows count];
while (count--) {
NSWindow *window = [windows objectAtIndex:count];
Document *document = [Document documentForWindow:window];
if (document) {
if ([document isDocumentEdited]) {
[document askToSave:@selector(reviewChangesAndQuitEnumeration:)];
return;
}
}
}
}
// if we get to here, either cont was YES and we reviewed all documents, or cont was NO and we don't want to quit
[NSApp replyToApplicationShouldTerminate:cont];
}
/* Loads the "encoding" accessory view used in save plain and open panels. There is a checkbox in the accessory which has different purposes in each case; so we let the caller set the title and other info for that checkbox.
*/
+ (NSView *)encodingAccessory:(unsigned)encoding includeDefaultEntry:(BOOL)includeDefaultItem encodingPopUp:(NSPopUpButton **)popup checkBox:(NSButton **)button {
OpenSaveAccessoryOwner *owner = [[[OpenSaveAccessoryOwner alloc] init] autorelease];
// Rather than caching, load the accessory view everytime, as it might appear in multiple panels simultaneously.
if (![NSBundle loadNibNamed:@"EncodingAccessory" owner:owner]) {
NSLog(@"Failed to load EncodingAccessory.nib");
return nil;
}
if (popup) *popup = owner->encodingPopUp;
if (button) *button = owner->checkBox;
[[EncodingManager sharedInstance] setupPopUp:owner->encodingPopUp selectedEncoding:encoding withDefaultEntry:includeDefaultItem];
return [owner->accessoryView autorelease];
}
+ (void)openWithEncodingAccessory:(BOOL)flag {
NSOpenPanel *panel = [NSOpenPanel openPanel];
BOOL ignoreRichPref = [[Preferences objectForKey:IgnoreRichText] boolValue];
BOOL ignoreHTMLPref = [[Preferences objectForKey:IgnoreHTML] boolValue];
NSButton *ignoreRichTextButton;
NSPopUpButton *encodingPopUp;
if (flag) {
[panel setAccessoryView:[self encodingAccessory:[[Preferences objectForKey:PlainTextEncodingForRead] intValue] includeDefaultEntry:YES encodingPopUp:&encodingPopUp checkBox:&ignoreRichTextButton]];
[ignoreRichTextButton setTitle:NSLocalizedString(@"Ignore rich text commands", @"Checkbox indicating that when opening a rich text file, the rich text should be ignored (causing the file to be loaded as plain text)")];
// Also set tooltip: "If selected, HTML and RTF files will be loaded as plain text, allowing you to see and edit the HTML or RTF directives"
// If the ignoreRichText and ignoreHTML preference values do not agree, then the initial state of the ignore button
// in the panel should be "mixed" state, indicating it will do the appropriate thing depending on the file selected
if (ignoreRichPref != ignoreHTMLPref) {
[ignoreRichTextButton setAllowsMixedState:YES];
[ignoreRichTextButton setState:NSMixedState];
} else {
if ([ignoreRichTextButton allowsMixedState]) [ignoreRichTextButton setAllowsMixedState:NO];
[ignoreRichTextButton setState:ignoreRichPref ? NSOnState : NSOffState];
}
}
[panel setAllowsMultipleSelection:YES];
if ([[Preferences objectForKey:OpenPanelFollowsMainWindow] boolValue]) [panel setDirectory:[Document directoryOfMainWindow]];
if ([panel runModal]) {
NSMutableArray *unopenedFilenames = nil;
NSArray *filenames = [panel filenames];
NSError *error;
unsigned cnt, numFiles = [filenames count];
if (flag) {
if ([ignoreRichTextButton state] == NSOffState) {
ignoreRichPref = ignoreHTMLPref = NO;
} else if ([ignoreRichTextButton state] == NSOnState) {
ignoreRichPref = ignoreHTMLPref = YES;
} // Otherwise they both remain at their respective state...
}
Document *previousDocument = nil; // We open multiply docs open front-to-back, for performance reasons
for (cnt = 0; cnt < numFiles; cnt++) {
NSString *filename = [filenames objectAtIndex:cnt];
Document *document = [Document openDocumentWithPath:filename encoding:flag ? [[encodingPopUp selectedItem] tag] : UnknownStringEncoding ignoreRTF:ignoreRichPref ignoreHTML:ignoreHTMLPref behind:previousDocument error:&error];
if (!document) {
if (!unopenedFilenames) unopenedFilenames = [NSMutableArray array];
if (!error) error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:[NSDictionary dictionaryWithObject:filename forKey:NSFilePathErrorKey]]; // If no error came back for some reason, create a generic one
[unopenedFilenames addObject:error];
} else {
previousDocument = document;
}
}
if (unopenedFilenames) [self displayOpenFailures:unopenedFilenames someSucceeded:([unopenedFilenames count] < numFiles)];
}
}
+ (void)open:(id)sender {
[self openWithEncodingAccessory:YES];
}
/* Put up panel indicating failure to open one or more files. errors is an array containing NSErrors for files which couldn't be opened; alertTitle is the title to be shown.
*/
+ (void)displayOpenFailures:(NSArray *)errors someSucceeded:(BOOL)someFilesOpened {
int numFailedFiles = [errors count];
if (numFailedFiles == 1) { // Just one file failed
[[NSAlert alertWithError:[errors objectAtIndex:0]] runModal];
} else {
// Choose appropriate string, based on whether all files failed or not
NSString *alertTitle = someFilesOpened ?
NSLocalizedString(@"Couldn\\U2019t open some of the specified files.", @"User tried to open more than one file; some (more than one) of them failed to open.") :
NSLocalizedString(@"Couldn\\U2019t open any of the specified files.", @"User tried to open more than one file; all of them failed to open.");
// Now include the file names along with failure reasons in the string
NSMutableString *errorMessage = [NSMutableString string];
int cnt, numFiles = [errors count];
if (numFiles > 5) numFiles = 3; // If many, just show a subset
for (cnt = 0; cnt < numFiles; cnt++) {
NSError *error = [errors objectAtIndex:cnt];
NSString *path = [[error userInfo] objectForKey:NSFilePathErrorKey];
if (path) {
NSString *reason = [error localizedFailureReason];
if (reason) {
[errorMessage appendFormat:NSLocalizedString(@"%@ (%@)", @"String for alert panel showing filename which failed to open followed by the reason why it failed to open, for instance: 'My File (You do not have enough access privileges)'"), displayName(path), reason];
} else {
[errorMessage appendString:displayName(path)];
}
if (cnt < numFiles-1) [errorMessage appendString:@"\n"];
}
}
if (numFiles < numFailedFiles) [errorMessage appendFormat:@"\n%@", NSLocalizedString(@"(And others)", @"Shown when many files failed to open. Shown at the end of the list of filenames, when there are too many to show.")];
(void)NSRunAlertPanel(alertTitle, @"%@", NSLocalizedString(@"OK", @"OK"), nil, nil, errorMessage);
}
}
/* Returns YES if the document can be closed. If the document is edited, puts up an alert and returns NO and gives the user a chance to save.
*/
- (BOOL)canCloseDocument {
if (isDocumentEdited) {
[self askToSave:NULL];
return NO;
}
return YES;
}
/* This method will put up an alert asking whether the document should be saved; if yes, then goes on to put up panels and such. The specified callback will be called with YES or NO at the end (NO if user cancelled the save).
*/
- (void)askToSave:(SEL)callback {
[[self window] makeKeyAndOrderFront:nil];
NSBeginAlertSheet(NSLocalizedString(@"Do you want to save changes to this document before closing?", @"Title in the alert panel when the user tries to close a window containing an unsaved document."),
NSLocalizedString(@"Save", @"Button choice which allows the user to save the document."),
NSLocalizedString(@"Don\\U2019t Save", @"Button choice which allows the user to cancel the save of a document."),
NSLocalizedString(@"Cancel", @"Button choice allowing user to cancel."),
[self window], self, @selector(willEndCloseSheet:returnCode:contextInfo:), @selector(didEndCloseSheet:returnCode:contextInfo:), (void *)callback,
NSLocalizedString(@"If you don\\U2019t save, your changes will be lost.", @"Subtitle in the alert panel when the user tries to close a window containing an unsaved document."));
}
/* We implement willEnd to check for NSAlertAlternateReturn here, because if the user indicates "close anyway," we want the window to go away immediately, rather than having the sheet slide up first.
*/
- (void)willEndCloseSheet:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
if (returnCode == NSAlertAlternateReturn) { /* "Don't Save" */
[[self window] close];
if (contextInfo) ((void (*)(id, SEL, BOOL))objc_msgSend)([self class], (SEL)contextInfo, YES); // Send callback (YES indicates continue saving)
}
}
- (void)didEndCloseSheet:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
if (returnCode == NSAlertDefaultReturn) { /* "Save" */
[self saveDocument:NO name:nil rememberName:YES shouldClose:YES whenDone:(SEL)contextInfo];
} else if (returnCode == NSAlertOtherReturn) { /* "Cancel" */
if (contextInfo) ((void (*)(id, SEL, BOOL))objc_msgSend)([self class], (SEL)contextInfo, NO); // Send callback indicating save cancelled
}
}
/* Saving */
/* The following few methods are for saving documents from the UI. The situation is a bit complicated due to full support for doc-modal sheets. Overall, here's the flow of save calls; the three main routines can put up panels which call the indented methods:
saveDocument:name:rememberName:shouldClose:whenDone: (calls getDocumentNameAndSave:)
if change doc format: didEndFormatWarningSheet:returnCode:contextInfo: (calls back into getDocumentNameAndSave:)
if change encoding: didEndEncodingSheet:returnCode:contextInfo: (calls back into getDocumentNameAndSave:)
getDocumentNameAndSave: (calls doSaveWithName:overwriteOK:)
if need file name: didEndSaveSheet:returnCode:contextInfo: (calls back into doSaveWithName:overwriteOK:)
doSaveWithName:overwriteOK:
if couldn't save file: didEndSaveErrorAlert:returnCode:contextInfo: (calls back into doSaveWithName:overwriteOK:)
*/
static DocumentSaveInfo *allocateDocumentContext(void) {
return malloc(sizeof(DocumentSaveInfo));
}
static void deallocateDocumentContext(DocumentSaveInfo *docInfo) {
[docInfo->nameForSaving release];
[docInfo->fileToBeRemoved release];
free(docInfo);
}
/* Action method for the "Append '.txt' extension" button
*/
- (void)appendPlainTextExtensionChanged:(id)sender {
NSSavePanel *panel = (NSSavePanel *)[sender window];
BOOL state = [sender state];
[panel setAllowsOtherFileTypes:state];
[panel setRequiredFileType:state ? @"txt" : @""];
}
/* Methods to deal with the rich text document format popup
For genstrings:
NSLocalizedString(@"Rich Text Format (RTF)", @"Name for Rich Text Format (RTF) documents to be displayed in the format popup of the save dialog")
NSLocalizedString(@"HTML", @"Name for HTML documents to be displayed in the format popup of the save dialog")
NSLocalizedString(@"Word Format", @"Name for Microsoft Word documents to be displayed in the format popup of the save dialog")
NSLocalizedString(@"Word XML Format", @"Name for Microsoft WordML documents to be displayed in the format popup of the save dialog")
NSLocalizedString(@"Rich Text With Graphics Format (RTFD)", @"Name for Rich Text With Graphics Format (RTFD) documents to be displayed in the format popup of the save dialog")
NSLocalizedString(@"Web Archive", @"Name for WebArchive documents to be displayed in the format popup of the save dialog")
*/
#define NumChooseableRichTextDocumentFormats 4
const static unsigned chooseableRichTextDocumentFormats[NumChooseableRichTextDocumentFormats] = {RichTextStringEncoding, HTMLStringEncoding, DocStringEncoding, WordMLStringEncoding};
NSString *chooseableRichTextDocumentFormatNames[NumChooseableRichTextDocumentFormats] = {@"Rich Text Format (RTF)", @"HTML", @"Word Format", @"Word XML Format"};
NSString *chooseableRichTextDocumentFormatFileTypes[NumChooseableRichTextDocumentFormats] = {@"rtf", @"html", @"doc", @"xml"};
#define NumChooseableRichTextWithGraphicsDocumentFormats 2
const static unsigned chooseableRichTextWithGraphicsDocumentFormats[NumChooseableRichTextDocumentFormats] = {RichTextWithGraphicsStringEncoding, WebArchiveStringEncoding};
NSString *chooseableRichTextWithGraphicsDocumentFormatNames[NumChooseableRichTextDocumentFormats] = {@"Rich Text With Graphics Format (RTFD)", @"Web Archive"};
NSString *chooseableRichTextWithGraphicsDocumentFormatFileTypes[NumChooseableRichTextDocumentFormats] = {@"rtfd", @"webarchive"};
- (void)setupRichTextDocumentFormatAccessory:(NSSavePanel *)panel withDocumentFormat:(unsigned)currentlySelectedDocumentFormat {
int cnt;
if (!richTextDocumentFormatAccessory) { // Loaded per document, as there might be multiple save panels active at any one time
if (![NSBundle loadNibNamed:@"RichTextDocumentFormatAccessory" owner:self]) {
NSLog(@"Failed to load RichTextDocumentFormatAccessory.nib");
return; // If for some reason the accessory can't be found, the user just won't be able to choose the document format
}
}
[richTextDocumentFormatPopUp removeAllItems];
for (cnt = 0; cnt < NumChooseableRichTextDocumentFormats; cnt++) [richTextDocumentFormatPopUp addItemWithTitle:NSLocalizedString(chooseableRichTextDocumentFormatNames[cnt], "")];
for (cnt = 0; cnt < NumChooseableRichTextDocumentFormats; cnt++) if (chooseableRichTextDocumentFormats[cnt] == currentlySelectedDocumentFormat) [richTextDocumentFormatPopUp selectItemAtIndex:cnt];
[richTextDocumentFormatPopUp setAction:@selector(richTextDocumentFormatChanged:)];
[panel setAccessoryView:richTextDocumentFormatAccessory];
[self richTextDocumentFormatChanged:richTextDocumentFormatPopUp]; // To initialize further
}
- (void)richTextDocumentFormatChanged:(id)sender {
NSSavePanel *panel = (NSSavePanel *)[sender window];
[panel setRequiredFileType:chooseableRichTextDocumentFormatFileTypes[[sender indexOfSelectedItem]]];
}
- (void)setupRichTextWithGraphicsDocumentFormatAccessory:(NSSavePanel *)panel withDocumentFormat:(unsigned)currentlySelectedDocumentFormat {
int cnt;
if (!richTextDocumentFormatAccessory) { // Loaded per document, as there might be multiple save panels active at any one time
if (![NSBundle loadNibNamed:@"RichTextDocumentFormatAccessory" owner:self]) {
NSLog(@"Failed to load RichTextDocumentFormatAccessory.nib");
return; // If for some reason the accessory can't be found, the user just won't be able to choose the document format
}
}
[richTextDocumentFormatPopUp removeAllItems];
for (cnt = 0; cnt < NumChooseableRichTextWithGraphicsDocumentFormats; cnt++) [richTextDocumentFormatPopUp addItemWithTitle:NSLocalizedString(chooseableRichTextWithGraphicsDocumentFormatNames[cnt], "")];
for (cnt = 0; cnt < NumChooseableRichTextWithGraphicsDocumentFormats; cnt++) if (chooseableRichTextWithGraphicsDocumentFormats[cnt] == currentlySelectedDocumentFormat) [richTextDocumentFormatPopUp selectItemAtIndex:cnt];
[richTextDocumentFormatPopUp setAction:@selector(richTextWithGraphicsDocumentFormatChanged:)];
[panel setAccessoryView:richTextDocumentFormatAccessory];
[self richTextWithGraphicsDocumentFormatChanged:richTextDocumentFormatPopUp]; // To initialize further
}
- (void)richTextWithGraphicsDocumentFormatChanged:(id)sender {
NSSavePanel *panel = (NSSavePanel *)[sender window];
[panel setRequiredFileType:chooseableRichTextWithGraphicsDocumentFormatFileTypes[[sender indexOfSelectedItem]]];
}
/* Returns the untitled document name for this document. The type argument, applicable only for rich documents, determines whether the extension for that type should be appended to the name. Pass in UnknownStringEncoding if not desired. For plain text documents, we add "txt" if user wanted it added, by having specified preference.
*/
- (NSString *)untitledDocumentName:(unsigned)type {
NSString *untitled;
if (untitledDocNumber == 0) untitledDocNumber = nextAvailableUntitledDocNumber();
if (untitledDocNumber > 1) {
// "LaunchTime" table contains the very few strings needed at launch time... A little performance optimization.
untitled = [NSString stringWithFormat:NSLocalizedStringFromTable(@"Untitled %d", @"LaunchTime", @"Name of new, untitled document with sequence number"), untitledDocNumber];
} else {
untitled = NSLocalizedStringFromTable(@"Untitled", @"LaunchTime", @"Name of new, untitled document");
}
if ([self isRichText]) { // Run through the types and pick the right extension
NSString *extension = nil;
int cnt;
if ([textStorage containsAttachments]) {
for (cnt = 0; cnt < NumChooseableRichTextWithGraphicsDocumentFormats && !extension; cnt++) if (chooseableRichTextWithGraphicsDocumentFormats[cnt] == type) extension = chooseableRichTextWithGraphicsDocumentFormatFileTypes[cnt];
} else {
for (cnt = 0; cnt < NumChooseableRichTextDocumentFormats && !extension; cnt++) if (chooseableRichTextDocumentFormats[cnt] == type) extension = chooseableRichTextDocumentFormatFileTypes[cnt];
}
if (extension) untitled = [untitled stringByAppendingPathExtension:extension];
} else if ([[Preferences objectForKey:AddExtensionToNewPlainTextFiles] boolValue]) {
untitled = [untitled stringByAppendingPathExtension:@"txt"];
}
return untitled;
}
/* Entry point for saving the document from the UI.
First checks to see if any warnings need to be shown; if so, does those; otherwise calls the save routine directly.
If showSavePanel is YES, of if a name is needed, the save panel will also be shown.
rememberNewNameAndSuch causes the document's name, encoding, etc to be reset after the save (basically this should be false for a "saveTo" operation and true otherwise).
shouldClose indicates whether the document should be closed if the save is successfully performed.
*/
- (void)saveDocument:(BOOL)showSavePanel rememberName:(BOOL)rememberNewNameAndSuch shouldClose:(BOOL)shouldClose {
[self saveDocument:showSavePanel name:nil rememberName:rememberNewNameAndSuch shouldClose:shouldClose whenDone:NULL];
}
/* If pathForSaving is nil, uses document's existing name.
*/
- (void)saveDocument:(BOOL)showSavePanel name:(NSString *)pathForSaving rememberName:(BOOL)rememberNewNameAndSuch shouldClose:(BOOL)shouldClose whenDone:(SEL)callback {
DocumentSaveInfo *docInfo;
if ([self hasSheet]) return; // This means we already have a sheet up. This is to prevent "Save All" type enumerations from getting into here.
docInfo = allocateDocumentContext();
docInfo->nameForSaving = pathForSaving ? [pathForSaving copy] : [[self documentName] copy];
docInfo->encodingForSaving = 0;
docInfo->haveToChangeType = NO;
docInfo->showEncodingAccessory = NO;
docInfo->rememberName = rememberNewNameAndSuch;
docInfo->shouldClose = shouldClose;
docInfo->showSavePanel = showSavePanel;
docInfo->whenDoneCallback = callback;
docInfo->hideExtension = FileExtensionPreviousState;
docInfo->encodingPopUp = nil;
docInfo->appendPlainTextExtensionButton = nil;
docInfo->showRichTextDocumentFormatAccessory = NO;
docInfo->showRichTextWithGraphicsDocumentFormatAccessory = NO;
docInfo->doingRTFDConversion = NO;
docInfo->fileToBeRemoved = nil;
if ([self isRichText]) { // Rich document case
if (docInfo->nameForSaving && [@"rtfd" isEqualToString:[docInfo->nameForSaving pathExtension]]) {
docInfo->encodingForSaving = RichTextWithGraphicsStringEncoding;
} else {
if ([textStorage containsAttachments]) {
if (documentEncoding == WebArchiveStringEncoding) docInfo->encodingForSaving = WebArchiveStringEncoding;
else docInfo->encodingForSaving = RichTextWithGraphicsStringEncoding;
} else {
if (documentEncoding == HTMLStringEncoding) docInfo->encodingForSaving = HTMLStringEncoding;
else if (documentEncoding == DocStringEncoding) docInfo->encodingForSaving = DocStringEncoding;
else if (documentEncoding == WordMLStringEncoding) docInfo->encodingForSaving = WordMLStringEncoding;
else if (documentEncoding == WebArchiveStringEncoding) docInfo->encodingForSaving = WebArchiveStringEncoding;
else if (documentEncoding == RichTextWithGraphicsStringEncoding) docInfo->encodingForSaving = RichTextWithGraphicsStringEncoding;
else docInfo->encodingForSaving = RichTextStringEncoding;
}
if ([self converted] || documentEncoding == SimpleTextStringEncoding) {
NSString *newFormatName = (docInfo->encodingForSaving == RichTextWithGraphicsStringEncoding) ?
NSLocalizedString(@"rich text with graphics (RTFD)", @"Rich text with graphics file format name, displayed in alert") :
NSLocalizedString(@"rich text", @"Rich text file format name, displayed in alert");
if ([self converted]) {
NSBeginAlertSheet(NSLocalizedString(@"Please supply a new name.", @"Title of alert panel which brings up a warning while saving, asking for new name"),
NSLocalizedString(@"Save with new name", @"Button choice allowing user to choose a new name"), NSLocalizedString(@"Cancel", @"Button choice allowing user to cancel."), nil,
[self window], self, NULL, @selector(didEndFormatWarningSheet:returnCode:contextInfo:), docInfo,
NSLocalizedString(@"This document was converted from a format that TextEdit cannot save. It will be saved in %@ format with a new name.", @"Contents of alert panel informing user that they need to supply a new file name because the file needs to be saved using a different format than originally read in"),
newFormatName);
} else {
NSString *oldFormatName = NSLocalizedString(@"SimpleText", @"SimpleText file format name, displayed in alert");
NSBeginAlertSheet(NSLocalizedString(@"Please supply a new name.", @"Title of alert panel which brings up a warning while saving, asking for new name"),
NSLocalizedString(@"Save with new name", @"Button choice allowing user to choose a new name"), NSLocalizedString(@"Cancel", @"Button choice allowing user to cancel."), nil,
[self window], self, NULL, @selector(didEndFormatWarningSheet:returnCode:contextInfo:), docInfo,
NSLocalizedString(@"TextEdit does not save files in %@ format; document will be saved in %@ format with a new name.", @"Contents of alert panel informing user that they need to supply a new file name because the file needs to be saved using a different format than originally read in"),
oldFormatName, newFormatName);
}
return;
} else if ((docInfo->encodingForSaving == RichTextWithGraphicsStringEncoding) && docInfo->nameForSaving && ![@"rtfd" isEqualToString:[docInfo->nameForSaving pathExtension]]) {
// At this point we will provide a warning about RTFD conversion, along with the ability to save with a new name (default button), or overwrite existing name (which will actually save .rtfd and remove .rtf)
NSString *defaultOption = NSLocalizedString(@"Save with new name", @"Button choice allowing user to choose a new name");
NSString *saveOption = NSLocalizedString(@"Save", @"Button choice which allows the user to save the document.");
if (docInfo->showSavePanel || [[NSFileManager defaultManager] fileExistsAtPath:[[docInfo->nameForSaving stringByDeletingPathExtension] stringByAppendingPathExtension:@"rtfd"]]) { // If we're showing the save panel anyway, or a file with the RTFD name already exists, just show one button allowing users to confirm the save
defaultOption = saveOption;
saveOption = nil;
}
docInfo->doingRTFDConversion = YES;
NSBeginAlertSheet(NSLocalizedString(@"Are you sure you want to save using RTFD format?", @"Title of alert panel which brings up a warning while saving"),
defaultOption, NSLocalizedString(@"Cancel", @"Button choice allowing user to cancel."), saveOption,
[self window], self, NULL, @selector(didEndFormatWarningSheet:returnCode:contextInfo:), docInfo,
NSLocalizedString(@"This document contains graphics and will be saved using RTFD (RTF with graphics) format. RTFD documents are not compatible with some applications. Save anyway?", @"Contents of alert panel informing user that the document is being converted from RTF to RTFD, and allowing them to cancel, save anyway, or save with new name"));
return;
} else if ([self lossy] && !showSavePanel) {
NSBeginAlertSheet(NSLocalizedString(@"Are you sure you want to overwrite the document?", @"Title of alert panel which brings up a warning about saving over the same document"),
NSLocalizedString(@"Save with new name", @"Button choice allowing user to choose a new name"),
NSLocalizedString(@"Cancel", @"Button choice allowing user to cancel."),
NSLocalizedString(@"Overwrite", @"Button choice allowing user to overwrite the document."),
[self window], self, NULL, @selector(didEndFormatWarningSheet:returnCode:contextInfo:), docInfo,
NSLocalizedString(@"Overwriting this document might cause you to lose some of the original formatting. Would you like to save the document using a new name?", @"Contents of alert panel informing user that they need to supply a new file name because the save might be lossy"));
return;
}
}
} else { // Plain document case; we have to figure out what character encoding to use
NSString *string = [textStorage string];
docInfo->showEncodingAccessory = YES;
docInfo->encodingForSaving = documentEncoding;
// If there is no existing encoding, or there is, but it is no longer suitable, clear it in preparation for computing a new one
if ((docInfo->encodingForSaving != UnknownStringEncoding) && ![string canBeConvertedToEncoding:docInfo->encodingForSaving]) {
docInfo->haveToChangeType = YES;
docInfo->encodingForSaving = UnknownStringEncoding;
}
if (docInfo->encodingForSaving == UnknownStringEncoding) {
// We try user's selected encoding, followed by their default language encoding, followed by Unicode (if in user's encodings list), followed by all other encodings in their encodings list; if none work Unicode is the last resort
NSStringEncoding defaultEncoding = [[Preferences objectForKey:PlainTextEncodingForWrite] intValue];
if ((defaultEncoding != UnknownStringEncoding) && [string canBeConvertedToEncoding:defaultEncoding]) {
docInfo->encodingForSaving = defaultEncoding;
} else {
NSStringEncoding defaultCStringEncoding = [NSString defaultCStringEncoding];
BOOL hasDefaultCStringEncoding = NO; // Whether user enabled this encoding
BOOL hasUnicodeEncoding = NO; // Whether user enabled this encoding
NSArray *encodings = [[EncodingManager sharedInstance] enabledEncodings];
int cnt, numEncodings = [encodings count];
for (cnt = 0; cnt < numEncodings; cnt++) {
NSStringEncoding encoding = [[encodings objectAtIndex:cnt] unsignedIntValue];
if (encoding == defaultCStringEncoding) hasDefaultCStringEncoding = YES;
if (encoding == NSUnicodeStringEncoding) hasUnicodeEncoding = YES;
}
if (hasDefaultCStringEncoding && [string canBeConvertedToEncoding:defaultCStringEncoding]) {
docInfo->encodingForSaving = defaultCStringEncoding;
} else if (hasUnicodeEncoding) {
docInfo->encodingForSaving = NSUnicodeStringEncoding;
} else for (cnt = 0; cnt < numEncodings; cnt++) { // Otherwise enumerate the encodings and find an appropriate one
NSStringEncoding encoding = [[encodings objectAtIndex:cnt] unsignedIntValue];
if ((encoding != defaultEncoding) && (encoding != NSUnicodeStringEncoding) && (encoding != NSUTF8StringEncoding) && (encoding != defaultCStringEncoding) && [string canBeConvertedToEncoding:encoding]) { // Skip over encodings we know work or we know we already checked
docInfo->encodingForSaving = encoding;
break;
}
}
}
if (docInfo->encodingForSaving == UnknownStringEncoding) docInfo->encodingForSaving = NSUnicodeStringEncoding; // Last resort...
if (docInfo->haveToChangeType) {
NSString *alertTitle = [NSString stringWithFormat:NSLocalizedString(@"This document can no longer be saved using its original %@ encoding.", @"Title of alert panel informing user that the file's string encoding needs to be changed."), [NSString localizedNameOfStringEncoding:documentEncoding]];
NSBeginAlertSheet(alertTitle, NSLocalizedString(@"OK", @"OK"), nil, nil,
[self window], self, NULL, @selector(didEndEncodingSheet:returnCode:contextInfo:), docInfo,
NSLocalizedString(@"Please choose another encoding (such as %@).", @"Subtitle of alert panel informing user that the file's string encoding needs to be changed"), [NSString localizedNameOfStringEncoding:docInfo->encodingForSaving]);
return;
}
}
}
// If we did not put up a sheet, then we get here, which means we can go on to the save panel
[self getDocumentNameAndSave:docInfo];
}
/* Called from the alert that asks user whether they're sure they want to save... If yes, we go onto put up a save panel
*/
- (void)didEndFormatWarningSheet:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
DocumentSaveInfo *docInfo = contextInfo;
if (returnCode == NSAlertDefaultReturn) { // "Save with new name"
docInfo->doingRTFDConversion = NO; // We clear this; the document is saved as a copy
[docInfo->nameForSaving release];
docInfo->nameForSaving = nil; // force user to provide a new name
[self getDocumentNameAndSave:docInfo];
} else if (returnCode == NSAlertOtherReturn) { // "Overwrite" (when applicable)
// Note that for RTF -> RTFD conversion, we still need to do a new name
if (docInfo->doingRTFDConversion) {
docInfo->fileToBeRemoved = docInfo->nameForSaving;
docInfo->nameForSaving = [[[docInfo->fileToBeRemoved stringByDeletingPathExtension] stringByAppendingPathExtension:@"rtfd"] copy];
}
[self getDocumentNameAndSave:docInfo];
} else { // "Cancel" --- NSAlertAlternateReturn
if (docInfo->whenDoneCallback) ((void (*)(id, SEL, BOOL))objc_msgSend)([self class], docInfo->whenDoneCallback, NO);
deallocateDocumentContext(docInfo);
}
}
/* Called from the alert that lets the user know that the encoding needs to change...
*/
- (void)didEndEncodingSheet:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
[self getDocumentNameAndSave:contextInfo];
}
/* Saves the document by first getting the document name (if needed)
*/
- (void)getDocumentNameAndSave:(DocumentSaveInfo *)docInfo {
if (docInfo->nameForSaving && !docInfo->haveToChangeType && !docInfo->showSavePanel) { // No save panel necessary!
[self doSaveWithName:docInfo overwriteOK:[[Preferences objectForKey:OverwriteReadOnlyFiles] boolValue]];
} else {
NSString *nameForSaving, *dirForSaving;
NSSavePanel *panel = [NSSavePanel savePanel];
BOOL updateEncoding = (docInfo->haveToChangeType || docInfo->showEncodingAccessory) ? YES : NO;
[panel setCanSelectHiddenExtension:YES];
switch (docInfo->encodingForSaving) {
case RichTextStringEncoding:
case HTMLStringEncoding:
case DocStringEncoding:
case WordMLStringEncoding:
[self setupRichTextDocumentFormatAccessory:panel withDocumentFormat:docInfo->encodingForSaving];
docInfo->showRichTextDocumentFormatAccessory = YES;
[panel setTitle:NSLocalizedString(@"Save Rich Text", @"Title of save and alert panels when saving rich text (RTF, Word, or others)")];
updateEncoding = NO;
break;
case RichTextWithGraphicsStringEncoding:
case WebArchiveStringEncoding:
[self setupRichTextWithGraphicsDocumentFormatAccessory:panel withDocumentFormat:docInfo->encodingForSaving];
docInfo->showRichTextWithGraphicsDocumentFormatAccessory = YES;
[panel setTitle:NSLocalizedString(@"Save Rich Text With Graphics", @"Title of save and alert panels when saving rich text with graphics (RTFD and WebArchive)")];
updateEncoding = NO;
break;
default:
[panel setTitle:NSLocalizedString(@"Save Plain Text", @"Title of save and alert panels when saving plain text")];
if ([[Preferences objectForKey:AddExtensionToNewPlainTextFiles] boolValue]) {
[panel setRequiredFileType:@"txt"];
[panel setAllowsOtherFileTypes:YES];
}
if (updateEncoding) {
NSString *string;
int cnt;
[panel setAccessoryView:[[self class] encodingAccessory:docInfo->encodingForSaving includeDefaultEntry:NO encodingPopUp:&(docInfo->encodingPopUp) checkBox:&(docInfo->appendPlainTextExtensionButton)]];
// Set up the checkbox
[docInfo->appendPlainTextExtensionButton setTitle:NSLocalizedString(@"If no extension is provided, use \\U201c.txt\\U201d.", @"Checkbox indicating that if the user does not specify an extension when saving a plain text file, .txt will be used")];
[docInfo->appendPlainTextExtensionButton setState:[[Preferences objectForKey:AddExtensionToNewPlainTextFiles] boolValue]];
[docInfo->appendPlainTextExtensionButton setAction:@selector(appendPlainTextExtensionChanged:)];
[docInfo->appendPlainTextExtensionButton setTarget:self];
// Further set up the encoding popup
cnt = [docInfo->encodingPopUp numberOfItems];
string = [textStorage string];
if (cnt * [string length] < 5000000) { // Otherwise it's just too slow; would be nice to make this more dynamic. With large docs and many encodings, the items just won't be validated.
while (cnt--) { // No reason go backwards accept to use one variable instead of two
int encoding = [[docInfo->encodingPopUp itemAtIndex:cnt] tag];
// Hardwire some encodings known to allow any content
if ((encoding != UnknownStringEncoding) && (encoding != NSUnicodeStringEncoding) && (encoding != NSUTF8StringEncoding) && (encoding != NSNonLossyASCIIStringEncoding) && ![string canBeConvertedToEncoding:encoding]) {
[[docInfo->encodingPopUp itemAtIndex:cnt] setEnabled:NO];
}
}
}
}
break;
}
[[self window] makeKeyAndOrderFront:nil];
nameForSaving = docInfo->nameForSaving ? [docInfo->nameForSaving lastPathComponent] : [self untitledDocumentName:docInfo->encodingForSaving];
if (docInfo->nameForSaving) {
dirForSaving = [docInfo->nameForSaving stringByDeletingLastPathComponent];
} else if ([[Preferences objectForKey:OpenPanelFollowsMainWindow] boolValue]) {
dirForSaving = [Document directoryOfMainWindow];
} else {
dirForSaving = nil; // Causes save panel to use the last folder that was visited
}
[panel beginSheetForDirectory:dirForSaving file:nameForSaving modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(didEndSaveSheet:returnCode:contextInfo:) contextInfo:docInfo];
}
}
/* Called when the save panel is dismissed. If OK, goes onto call doSaveW |