/* FileManager.m Implementation of the FileManager class for the ProjectManager application. Copyright (C) 2005 Saso Kiselkov This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #import "FileManager.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "ProjectImageView.h" #import "ProjectBrowser.h" #import "FileManagerDelegate.h" #import "../../ProjectDocument.h" #import "../../RelativePathUtilities.h" #import "../../ProjectCreator.h" #import "../../NSDictionaryAdditions.h" #import "../../NSArrayAdditions.h" #import "../../NSImageAdditions.h" #import "TemplateFileSelector.h" #import "UtilityFunctions.h" static NSString * const ProjectFileTypePlain = @"Plain"; static NSString * const ProjectFileTypeLink = @"Link"; static NSString * const ProjectFileTypeCategory = @"Category"; static NSString * const ProjectFileTypeVirtual = @"Virtual"; NSString * const ProjectFilesPboardType = @"ProjectFilesPboardType"; NSString * const ProjectFilesDidChangeNotification = @"ProjectFilesDidChangeNotification"; NSString * const ProjectFilesErrorDomain = @"ProjectFilesErrorDomain"; /** * Test whether a specified file is a text file. * * The test is one by reading the first 4096 bytes of the file * and checking whether all the bytes are non-null. * * @return YES if the file is a text file, NO if it isn't. */ static BOOL CheckTextFile (NSString * filename) { NSFileHandle * fh; NSData * data; unsigned int i, n; const char * buf; fh = [NSFileHandle fileHandleForReadingAtPath: filename]; if (fh == nil) { return NO; } data = [fh readDataOfLength: 4096]; buf = [data bytes]; for (i = 0, n = [data length]; i < n; i++) { if (!buf[i]) { return NO; } } return YES; } /** * Converts from an external FMFileType file type representation to an * internal NSString-based. * * @param type The type which to convert. * * @return A string representation of the equivalent internal file type, * or `nil' if the passed file type is invalid. */ static inline NSString * InternalFileTypeFromExternal (FMFileType type) { switch (type) { case FMFileTypePlain: return ProjectFileTypePlain; case FMFileTypeLink: return ProjectFileTypeLink; case FMFileTypeCategory: return ProjectFileTypeCategory; case FMFileTypeVirtual: return ProjectFileTypeVirtual; default: return nil; } } /** * Converts from an internal NSString-based file type representation to * an external FMFileType-based one. * * @param type The internal representation which to convert. * * @return An equivalent external type, or -1 if the passed type is invalid. */ static inline FMFileType ExternalFileTypeFromInternal (NSString * type) { if ([type isEqualToString: ProjectFileTypePlain]) { return FMFileTypePlain; } else if ([type isEqualToString: ProjectFileTypeLink]) { return FMFileTypeLink; } else if ([type isEqualToString: ProjectFileTypeCategory]) { return FMFileTypeCategory; } else if ([type isEqualToString: ProjectFileTypeVirtual]) { return FMFileTypeVirtual; } else { return -1; } } /** * Locates files of a specific type in a category contents array and * notes their names in a specific array. This function is used in * the -[FileManager filesAtPath:ofType:recursively:] method in order * to perform the actual search. * * @param searchCategoryContents The category contents array which to search. * @param outputArray A mutable array to which the filenames found will * be written. Please note that only the name of the file is written, * not it's entire path. * @param aFileType The required file type which to search for. * @param recursive A flag which specifies whether subcategories should * be searched as well. */ static void LocateFilesOfType (NSArray * searchCategoryContents, NSMutableArray * outputArray, NSString * aFileType, BOOL recursive) { NSEnumerator * e; NSDictionary * entry; e = [searchCategoryContents objectEnumerator]; while ((entry = [e nextObject]) != nil) { NSString * fileType = [entry objectForKey: @"Type"]; if ([fileType isEqualToString: aFileType]) { [outputArray addObject: [entry objectForKey: @"Name"]]; } if (recursive && [fileType isEqualToString: ProjectFileTypeCategory]) { LocateFilesOfType ([entry objectForKey: @"Contents"], outputArray, aFileType, YES); } } } @interface FileManager (Private) - (BOOL) validateAction: (SEL) anAction; - (NSMutableDictionary *) fileEntryAtPath: (NSString *) aPath; - (NSMutableArray *) categoryContentsArrayAtPath: (NSString *) aPath; - (BOOL) copyPlainFileAtPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error; - (BOOL) copyLinkAtPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error; - (BOOL) copyCategoryAtPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error; - (BOOL) movePlainFileAtPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error; - (BOOL) moveLinkAtPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error; - (BOOL) moveCategoryAtPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error; - (BOOL) performFileClashCheckFromPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error; - (void) addEntryAtPath: (NSString *) aPath ofType: (NSString *) fileType withArgument: (NSString *) anArgument; - (void) removeEntryAtPath: (NSString *) aPath; - (NSString *) makeNewUniqueNameFromBasename: (NSString *) basename pathExtension: (NSString *) ext inCategory: (NSString *) category andDirectory: (NSString *) directory; - (NSString *) recursivelyLocateFileAtPhysicalPath: (NSString *) diskLocation inCategory: (NSString *) aCategory; - (NSString *) internalTypeOfFileAtPath: (NSString *) aPath; - (BOOL) internalImportFile: (NSString *) filePath renameTo: (NSString *) newName toPath: (NSString *) category link: (BOOL) linkFlag error: (NSError **) error; @end @implementation FileManager (Private) /** * Validates an action. This is used to unify validation of toolbar * items and menu items into a single routine. * * @param action The action which to validate. * * @return YES if the action is valid, NO if it isn't. */ - (BOOL) validateAction: (SEL) action { // don't enable our controls when we're not visible if ([document currentProjectModule] != self) { return NO; } if (sel_eq(action, @selector(importFiles:)) || sel_eq(action, @selector(newEmptyFile:)) || sel_eq(action, @selector(newFileFromTemplate:))) { return [delegate canCreatePlainFilesAtPath: [self containingCategory]]; } else if (sel_eq(action, @selector(newCategory:))) { return [delegate canCreateCategoriesAtPath: [self containingCategory]]; } else if (sel_eq(action, @selector(deleteFiles:))) { NSArray * selectedFiles; NSEnumerator * e; NSString * filepath; selectedFiles = [self selectedFiles]; if ([selectedFiles count] == 0) { return NO; } e = [selectedFiles objectEnumerator]; while ((filepath = [e nextObject]) != nil) { if (![delegate canDeletePath: filepath]) { return NO; } } return YES; } else { return YES; } } /** * Copies a plain file in the project. * * @param aPath The path from which to copy the file. * @param newPath The path to which to copy the file. * @param error A pointer to an NSError variable which will be filled with * an error in description in case an error occurs during the operation. * * @return YES if the operation succeeds, NO if it doesn't. */ - (BOOL) copyPlainFileAtPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error { NSFileManager * fm = [NSFileManager defaultManager]; NSString * srcPath = [delegate pathToFile: aPath isCategory: NO], * destPath = [delegate pathToFile: newPath isCategory: NO]; if (!CreateDirectoryAndIntermediateDirectories([destPath stringByDeletingLastPathComponent], error)) { return NO; } if (![fm copyPath: srcPath toPath: destPath handler: nil]) { SetFileError (error, ProjectFilesCopyError, _(@"Failed to copy the file %@."), srcPath); return NO; } [self addEntryAtPath: newPath ofType: ProjectFileTypePlain withArgument: nil]; return YES; } /** * Copies a link in the project. * * @param aPath The path from which to copy the link. * @param newPath The path to which to copy the link. * @param error A pointer to an NSError variable which will be filled with * an error in description in case an error occurs during the operation. * * @return YES if the operation succeeds, NO if it doesn't. */ - (BOOL) copyLinkAtPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error { NSString * srcPath = [delegate pathToFile: aPath isCategory: NO], * destPath = [delegate pathToFile: newPath isCategory: NO]; NSString * linkTarget = [self targetOfLinkAtPath: aPath]; linkTarget = TranslocateLinkTarget(linkTarget, srcPath, destPath); #ifdef HAVE_SYMLINKS if (![fm createSymbolicLinkAtPath: destPath pathContent: linkTarget]) { SetFileError (error, ProjectFilesCreationError, _(@"Couldn't create link at path %@."), destPath); return NO; } #endif [self addEntryAtPath: newPath ofType: ProjectFileTypeLink withArgument: linkTarget]; return YES; } /** * Copies a category recursively in the project. * * @param aPath The path from which to copy the category. * @param newPath The path to which to copy the category. * @param error A pointer to an NSError variable which will be filled with * an error in description in case an error occurs during the operation. * * @return YES if the operation succeeds, NO if it doesn't. */ - (BOOL) copyCategoryAtPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error { NSEnumerator * e; NSString * filename; [self addEntryAtPath: newPath ofType: ProjectFileTypeCategory withArgument: nil]; e = [[self filesAtPath: aPath] objectEnumerator]; while ((filename = [e nextObject]) != nil) { if (![self copyPath: [aPath stringByAppendingPathComponent: filename] toPath: [newPath stringByAppendingPathComponent: filename] error: error]) { return NO; } } return YES; } /** * Moves a plain file in the project. * * @param aPath The path from which to move the file. * @param newPath The path to which to move the file. * @param error A pointer to an NSError variable which will be filled with * an error in description in case an error occurs during the operation. * * @return YES if the operation succeeds, NO if it doesn't. */ - (BOOL) movePlainFileAtPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error { NSFileManager * fm = [NSFileManager defaultManager]; NSString * srcPath = [delegate pathToFile: aPath isCategory: NO], * destPath = [delegate pathToFile: newPath isCategory: NO]; if (![srcPath isEqualToString: destPath]) { if (!CreateDirectoryAndIntermediateDirectories([destPath stringByDeletingLastPathComponent], error)) { return NO; } if (![fm movePath: srcPath toPath: destPath handler: nil]) { SetFileError (error, ProjectFilesMoveError, _(@"Failed to move the file %@."), srcPath); return NO; } } [self addEntryAtPath: newPath ofType: ProjectFileTypePlain withArgument: nil]; [self removeEntryAtPath: aPath]; return YES; } /** * Moves a link in the project. The link's target will be recomputed in * order to keep the link valid. * * @param aPath The path from which to move the link. * @param newPath The path to which to move the link. * @param error A pointer to an NSError variable which will be filled with * an error in description in case an error occurs during the operation. * * @return YES if the operation succeeds, NO if it doesn't. */ - (BOOL) moveLinkAtPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error { NSString * srcPath = [delegate pathToFile: aPath isCategory: NO], * destPath = [delegate pathToFile: newPath isCategory: NO]; NSString * linkTarget = [self targetOfLinkAtPath: aPath]; linkTarget = TranslocateLinkTarget(linkTarget, srcPath, destPath); #ifdef HAVE_SYMLINKS if (![fm createSymbolicLinkAtPath: destPath pathContent: linkTarget]) { SetFileError (error, ProjectFilesCreationError, _(@"Couldn't create link at path %@."), destPath); return NO; } if (![fm removeFileAtPath: srcPath handler: nil]) { SetFileError (error, ProjectFilesDeletionError, _(@"Couldn't delete the original link at path %@."), srcPath); return NO; } #endif [self addEntryAtPath: newPath ofType: ProjectFileTypeLink withArgument: linkTarget]; [self removeEntryAtPath: aPath]; return YES; } /** * Moves a category recursively in the project. * * @param aPath The path from which to move the category. * @param newPath The path to which to move the category. * @param error A pointer to an NSError variable which will be filled with * an error in description in case an error occurs during the operation. * * @return YES if the operation succeeds, NO if it doesn't. */ - (BOOL) moveCategoryAtPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error { NSEnumerator * e; NSString * filename; [self addEntryAtPath: newPath ofType: ProjectFileTypeCategory withArgument: nil]; e = [[self filesAtPath: aPath] objectEnumerator]; while ((filename = [e nextObject]) != nil) { if (![self movePath: [aPath stringByAppendingPathComponent: filename] toPath: [newPath stringByAppendingPathComponent: filename] error: error]) { return NO; } } [self removeEntryAtPath: aPath]; return YES; } /** * Performs a check whether the file at path `aPath' doesn't clash * with an file if it were to exist at path `newPath'. For the various * file types this means: * * - virtual files: check whether the file doesn't already exist in * the destination category * - plain files and links: same as for virtual files, and additionally * check that there's no underlying disk file in the way * - categories: same as for virtual files, and additionally perform * this method for all it's descendents recursively * * @return YES if there is no clash, NO if there is one. */ - (BOOL) performFileClashCheckFromPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error { NSString * fileType; if ([self fileExistsAtPath: newPath]) { SetFileError (error, ProjectFilesAlreadyExistError, _(@"File already exists at %@."), newPath); return NO; } fileType = [self internalTypeOfFileAtPath: aPath]; if ([fileType isEqualToString: ProjectFileTypePlain] || [fileType isEqualToString: ProjectFileTypeLink]) { NSString * newDiskPath = [delegate pathToFile: newPath isCategory: NO], * oldDiskPath = [delegate pathToFile: aPath isCategory: NO]; if ([[NSFileManager defaultManager] fileExistsAtPath: newDiskPath] && ![newDiskPath isEqualToString: oldDiskPath]) { SetFileError (error, ProjectFilesAlreadyExistError, _(@"The file %@ cannot be stored\n" @"on disk at %@. Another file is in the way.\n" @"Please delete that file first and try again."), aPath, newDiskPath); return NO; } else { return YES; } } else if ([fileType isEqualToString: ProjectFileTypeCategory]) { NSEnumerator * e = [[self filesAtPath: aPath] objectEnumerator]; NSString * filename; while ((filename = [e nextObject]) != nil) { if (![self performFileClashCheckFromPath: [aPath stringByAppendingPathComponent: filename] toPath: [newPath stringByAppendingPathComponent: filename] error: error]) { return NO; } } return YES; } else if ([fileType isEqualToString: ProjectFileTypeVirtual]) { return YES; } // should never occur [NSException raise: NSInternalInconsistencyException format: _(@"Unknown file type %i encountered at %@."), fileType, aPath]; return NO; } /** * Returns the file entry which represents `aPath' in the * file management dictionary. * * @return the entry if it is found, or `nil' if it isn't. */ - (NSMutableDictionary *) fileEntryAtPath: (NSString *) aPath { NSString * filename = [aPath lastPathComponent], * category = [aPath stringByDeletingLastPathComponent]; NSEnumerator * e; NSMutableDictionary * entry; e = [[self categoryContentsArrayAtPath: category] objectEnumerator]; while ((entry = [e nextObject]) != nil) { if ([[entry objectForKey: @"Name"] isEqualToString: filename]) { return entry; } } // not found return nil; } /** * Returns the contents array of the category at path `aPath'. * * @return the category's contents array if it is found, or `nil' * if it isn't. */ - (NSMutableArray *) categoryContentsArrayAtPath: (NSString *) aPath { NSArray * pathComponents; NSEnumerator * e; NSString * pathComponent; NSMutableArray * array; // standardize the path aPath = [aPath stringByStandardizingPath]; pathComponents = [aPath pathComponents]; e = [pathComponents objectEnumerator]; // handle the case whether path contains the leading '/' (or whatever // is the root indicator on your platform). if ([aPath isAbsolutePath]) { if ([pathComponents count] == 1) { return files; } else { // skip the root indicator [e nextObject]; } } array = files; while ((pathComponent = [e nextObject]) != nil) { NSEnumerator * ee = [array objectEnumerator]; NSMutableDictionary * entry; while ((entry = [ee nextObject]) != nil) { if ([[entry objectForKey: @"Name"] isEqualToString: pathComponent]) { break; } } if (entry != nil) { array = [entry objectForKey: @"Contents"]; if (array == nil) { array = [NSMutableArray array]; [entry setObject: array forKey: @"Contents"]; } } // a path component was not found else { return nil; } } return array; } /** * Adds an entry at `aPath' with the file type set to `aFileType'. * In case the created file is a link, `anArgument' should contain * the link's target. */ - (void) addEntryAtPath: (NSString *) aPath ofType: (NSString *) aFileType withArgument: (NSString *) anArgument { NSString * filename = [aPath lastPathComponent], * category = [aPath stringByDeletingLastPathComponent]; NSMutableDictionary * entry; entry = [NSMutableDictionary dictionaryWithObjectsAndKeys: filename, @"Name", aFileType, @"Type", nil]; if ([aFileType isEqualToString: ProjectFileTypeLink]) { [entry setObject: anArgument forKey: @"Target"]; } [[self categoryContentsArrayAtPath: category] addObject: entry]; } /** * Removes a file system dictionary entry. * * @param aPath A path to the file who's entry to remove. */ - (void) removeEntryAtPath: (NSString *) aPath { NSString * filename = [aPath lastPathComponent], * category = [aPath stringByDeletingLastPathComponent]; NSMutableArray * array = [self categoryContentsArrayAtPath: category]; unsigned int i, n; for (i = 0, n = [array count]; i < n; i++) { NSDictionary * entry = [array objectAtIndex: i]; if ([[entry objectForKey: @"Name"] isEqualToString: filename]) { [array removeObjectAtIndex: i]; break; } } } /** * Makes a new name from `basename' so that it is unique in `category' * and `directory'. E.g. basename = @"New File", then the method will * check whether * @"New File" * @"New File 1" * @"New File 2" * ... * is unique in the provided category and directory. The first of these * names which already is unique will be returned. */ - (NSString *) makeNewUniqueNameFromBasename: (NSString *) basename pathExtension: (NSString *) ext inCategory: (NSString *) category andDirectory: (NSString *) directory { NSString * newName; unsigned int i; NSArray * dirContents; NSFileManager * fm = [NSFileManager defaultManager]; // make @"" behave as if ext = nil if ([ext length] == 0) { ext = nil; } if (ext != nil) { newName = [basename stringByAppendingPathExtension: ext]; } else { newName = basename; } if ([self fileExistsAtPath: [category stringByAppendingPathComponent: newName]] == NO && [fm fileExistsAtPath: [directory stringByAppendingPathComponent: newName]] == NO) { return newName; } dirContents = [fm directoryContentsAtPath: directory]; i = 1; do { if (ext != nil) { newName = [NSString stringWithFormat: @"%@ %i.%@", basename, i, ext]; } else { newName = [NSString stringWithFormat: @"%@ %i", basename, i]; } i++; } while ([self fileExistsAtPath: [category stringByAppendingPathComponent: newName]] || [dirContents containsObject: newName]); return newName; } /** * This method recursively searches for a file based on it's physical * on-disk location in a certain category and it's descendents. * * @param diskLocation The physical location of the file. * @param aCategory The category in which to recursively look for the file. * * @return An in-project path to a file who's physical location is that * indicated by the first argument. If no such file exists in the * project, `nil' is returned instead. */ - (NSString *) recursivelyLocateFileAtPhysicalPath: (NSString *) diskLocation inCategory: (NSString *) aCategory { NSArray * fileNames; NSEnumerator * e; NSString * fileName; fileNames = [self filesAtPath: aCategory]; e = [fileNames objectEnumerator]; while ((fileName = [e nextObject]) != nil) { NSString * filePath = [aCategory stringByAppendingPathComponent: fileName]; BOOL isCategory = [[self internalTypeOfFileAtPath: filePath] isEqualToString: ProjectFileTypeCategory]; NSString * diskPath; diskPath = [delegate pathToFile: filePath isCategory: isCategory]; if ([diskPath isEqualToString: diskLocation]) { return diskPath; } else if (isCategory) { diskPath = [self recursivelyLocateFileAtPhysicalPath: diskLocation inCategory: filePath]; if (diskPath != nil) { return diskPath; } } } return nil; } - (NSString *) internalTypeOfFileAtPath: (NSString *) aPath { if ([aPath isEqualToString: @"/"]) { return ProjectFileTypeCategory; } else { return [[self fileEntryAtPath: aPath] objectForKey: @"Type"]; } } /** * The actual implementation of the * -[FileManager importFile:renameTo:toPath:link:error:] method. */ - (BOOL) internalImportFile: (NSString *) filePath renameTo: (NSString *) newName toPath: (NSString *) category link: (BOOL) linkFlag error: (NSError **) error { NSString * fileName = [filePath lastPathComponent]; NSString * destPath = [category stringByAppendingPathComponent: newName]; NSString * diskDestPath = [delegate pathToFile: destPath isCategory: NO]; NSFileManager * fm = [NSFileManager defaultManager]; NSString * symlinkPath = nil; if (diskDestPath == nil) { SetFileError (error, ProjectFilesInvalidFileTypeError, _(@"The specified category doesn't exist on-disk, cannot add " @"disk files to it.")); return NO; } if ([self fileExistsAtPath: [category stringByAppendingPathComponent: fileName]]) { SetFileError (error, ProjectFilesAlreadyExistError, _(@"File already exists.")); return NO; } if (![diskDestPath isEqualToString: filePath]) { if ([fm fileExistsAtPath: diskDestPath]) { NSString * location; // TODO // try to locate the file in the project and tell the user where // the conflict originated, if possible /* location = [self pathToFileAtPhysicalPath: diskDestPath]; if (location != nil) { NSRunAlertPanel(_(@"Cannot import file"), _(@"A file named %@ in %@ is in the way."), nil, nil, nil, fileName, [location stringByDeletingLastPathComponent]); } // otherwise simply report that there is a file in the way else {*/ SetFileError (error, ProjectFilesAlreadyExistError, _(@"A file named %@ is in the way."), fileName); // } return NO; } if (!CreateDirectoryAndIntermediateDirectories([diskDestPath stringByDeletingLastPathComponent], error)) { return NO; } // copy the file if (linkFlag == NO) { if (!ImportProjectFile (filePath, diskDestPath, [document projectName], error)) { return NO; } } else { symlinkPath = filePath; // otherwise, if the target system has support for symbolic // links, create a disk link to the file #ifdef HAVE_SYMLINKS if ([fm createSymbolicLinkAtPath: destDiskPath pathContent: symlinkPath] == NO) { SetFileError (error, ProjectFilesCreationError, _(@"Failed to create a link to the file %@ from the project."), fileName); return NO; } #endif } } // The file already is in the correct location, but make sure it // isn't already included in some other project category - this could // create inconsistencies with copying/moving files around categories. else { NSString * location = [self pathToFileAtPhysicalPath: diskDestPath]; if (location != nil) { SetFileError (error, ProjectFilesAlreadyExistError, _(@"The file %@ already exists in the project, in category %@."), fileName, [location stringByDeletingLastPathComponent]); return NO; } } [self addEntryAtPath: destPath ofType: linkFlag ? ProjectFileTypeLink : ProjectFileTypePlain withArgument: symlinkPath]; [document updateChangeCount: NSChangeDone]; PostFilesChangedNotification (self, category); return YES; } @end @implementation FileManager + (NSString *) moduleName { return @"FileManager"; } + (NSString *) humanReadableModuleName { return _(@"Files"); } - (void) dealloc { [[NSNotificationCenter defaultCenter] removeObserver: self]; TEST_RELEASE(view); TEST_RELEASE(files); [super dealloc]; } - initWithDocument: (ProjectDocument *) doc infoDictionary: (NSDictionary *) infoDict { if ((self = [self init]) != nil) { document = doc; ASSIGN(files, [[infoDict objectForKey: @"Files"] makeDeeplyMutableEquivalent]); [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(projectNameChanged:) name: ProjectNameDidChangeNotification object: document]; } return self; } - (void) finishInit { delegate = (id ) [document projectType]; } - delegate { return delegate; } - (ProjectDocument *) document { return document; } - (NSView *) view { if (view == nil) { [NSBundle loadNibNamed: @"FileManager" owner: self]; } return view; } - (NSDictionary *) infoDictionary { return [NSDictionary dictionaryWithObject: files forKey: @"Files"]; } - (void) awakeFromNib { [view retain]; [view removeFromSuperview]; DESTROY(bogusWindow); [browser setDoubleAction: @selector(openFile:)]; [browser setMaxVisibleColumns: 4]; [self selectFile: browser]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(filesChanged:) name: ProjectFilesDidChangeNotification object: self]; } - (void) openFile: (id) sender { NSEnumerator * e = [[self selectedFiles] objectEnumerator]; NSString * path; while ((path = [e nextObject]) != nil) { NSString * fileType = [self internalTypeOfFileAtPath: path]; FileOpenResult result; result = [delegate openFile: path]; if (result == FileOpenCannotHandle) { if (![fileType isEqualToString: ProjectFileTypeCategory] && ![fileType isEqualToString: ProjectFileTypeVirtual]) { result = [self openPath: path]; } } if (result == FileOpenFailure) { if (NSRunAlertPanel(_(@"Failed to open file"), _(@"Failed to open file %@."), _(@"OK"), _(@"Cancel"), nil, path) == NSAlertAlternateReturn) { break; } } } } - (void) browser: (NSBrowser *) sender createRowsForColumn: (int) column inMatrix: (NSMatrix *) matrix { NSString * path = [browser path]; NSEnumerator * e; NSString * name; NSFont * boldFont = [NSFont boldSystemFontOfSize: 0]; e = [[[self filesAtPath: path] sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)] objectEnumerator]; while ((name = [e nextObject]) != nil) { NSBrowserCell * cell; [matrix addRow]; cell = [matrix cellAtRow: [matrix numberOfRows] - 1 column: 0]; [cell setTitle: name]; if ([[self internalTypeOfFileAtPath: [path stringByAppendingPathComponent: name]]isEqualToString: ProjectFileTypeCategory]) { [cell setLeaf: NO]; [cell setFont: boldFont]; } else { [cell setLeaf: YES]; } } } - (NSString *) browser: (NSBrowser *)sender titleOfColumn: (int)column { if (column == 0) { return [document projectName]; } else { return [[sender selectedCellInColumn: column - 1] title]; } } - (void) selectFile: sender { // this needs special treatment if ([[browser path] isEqualToString: @"/"]) { [fileIcon setImage: [NSImage imageNamed: @"File_project"]]; [fileIcon setShowsLinkIndicator: NO]; [fileNameField setStringValue: [document projectName]]; [filePathField setStringValue: [document projectDirectory]]; [fileSizeField setStringValue: MakeSizeStringFromValue([self measureDiskUsageAtPath: @"/"])]; [fileTypeField setStringValue: _(@"Project")]; [lastModifiedField setStringValue: nil]; SetTextFieldEnabled (fileNameField, YES); } else { NSArray * selectedFiles = [self selectedFiles]; if ([selectedFiles count] > 1) { unsigned long long size = 0; NSEnumerator * e; NSString * entry; NSString * containingCategory = [self containingCategory]; [fileIcon setImage: [NSImage imageNamed: @"MultipleSelection" owner: self]]; [fileIcon setShowsLinkIndicator: NO]; [fileNameField setStringValue: [NSString stringWithFormat: _(@"%i Elements"), [[browser selectedCells] count]]]; SetTextFieldEnabled(fileNameField, NO); [filePathField setStringValue: nil]; e = [selectedFiles objectEnumerator]; for (size = 0; (entry = [e nextObject]) != nil; size += [self measureDiskUsageAtPath: entry]); [fileSizeField setStringValue: MakeSizeStringFromValue(size)]; [fileTypeField setStringValue: nil]; [lastModifiedField setStringValue: nil]; } else if ([selectedFiles count] == 1) { NSFileManager * fm = [NSFileManager defaultManager]; NSString * selectedFile = [selectedFiles objectAtIndex: 0]; NSImage * icon = [self iconForPath: selectedFile]; NSString * fileType; [fileIcon setImage: icon]; [fileNameField setStringValue: [selectedFile lastPathComponent]]; [fileSizeField setStringValue: MakeSizeStringFromValue([self measureDiskUsageAtPath: selectedFile])]; SetTextFieldEnabled(fileNameField, [delegate canDeletePath: selectedFile]); fileType = [self internalTypeOfFileAtPath: selectedFile]; if ([fileType isEqualToString: ProjectFileTypePlain]) { [fileIcon setShowsLinkIndicator: NO]; [filePathField setStringValue: [delegate pathToFile: selectedFile isCategory: NO]]; [fileTypeField setStringValue: _(@"File")]; [lastModifiedField setObjectValue: [[fm fileAttributesAtPath: [delegate pathToFile: selectedFile isCategory: NO] traverseLink: NO] fileModificationDate]]; } else if ([fileType isEqualToString: ProjectFileTypeLink]) { [fileIcon setShowsLinkIndicator: YES]; [filePathField setStringValue: [self targetOfLinkAtPath: selectedFile]]; [fileTypeField setStringValue: _(@"Link")]; #ifdef HAVE_SYMLINKS [lastModifiedField setStringValue: [[fm fileAttributesAtPath: [delegate pathToFile: selectedFile isCategory: NO] traverseLink: NO] fileModificationDate]]; #endif } else if ([fileType isEqualToString: ProjectFileTypeCategory]) { [fileIcon setShowsLinkIndicator: NO]; [filePathField setStringValue: [delegate pathToFile: selectedFile isCategory: YES]]; [fileTypeField setStringValue: _(@"Project Category")]; [lastModifiedField setStringValue: nil]; } else if ([fileType isEqualToString: ProjectFileTypeVirtual]) { [fileIcon setShowsLinkIndicator: NO]; [filePathField setStringValue: nil]; [fileTypeField setStringValue: _(@"Virtual File")]; [lastModifiedField setStringValue: nil]; } } else { [fileIcon setImage: nil]; [fileIcon setShowsLinkIndicator: NO]; [fileNameField setStringValue: nil]; SetTextFieldEnabled(fileNameField, NO); [filePathField setStringValue: nil]; [fileSizeField setStringValue: nil]; [fileTypeField setStringValue: nil]; [lastModifiedField setStringValue: nil]; } } } /** * Instructs the browser to set it's path to `aPath', selects name * of the entry in the fileName text field and allows the user to * edit it. */ - (void) selectAndEditNameAtPath: (NSString *) aPath { [browser setPath: aPath]; [self selectFile: nil]; [fileNameField selectText: nil]; } /** * Action sent when the selected file's name in the fileName * text field is edited by the user. */ - (void) changeName: sender { NSString * newName = [fileNameField stringValue]; // editing the project name? if ([[browser path] isEqualToString: @"/"]) { [document setProjectName: newName]; } else { if (![newName isEqualToString: [[browser path] lastPathComponent]]) { NSError * error; NSString * previousPath = [[self selectedFiles] objectAtIndex: 0], * newPath = [[previousPath stringByDeletingLastPathComponent] stringByAppendingPathComponent: newName]; if ([self movePath: previousPath toPath: newPath error: &error]) { [browser setPath: newPath]; [self selectFile: browser]; } else { DescribeError (error, _(@"Cannot rename file"), _(@"Could not rename file %@"), nil, nil, nil, nil, [previousPath lastPathComponent]); } } } } /** * Returns the paths to the files that are currently selected, otherwise nil. */ - (NSArray *) selectedFiles { NSMutableArray * array = [NSMutableArray array]; NSEnumerator * e = [[browser selectedCells] objectEnumerator]; NSBrowserCell * cell; NSString * pathPrefix = [browser pathToColumn: [browser selectedColumn]]; while ((cell = [e nextObject]) != nil) { [array addObject: [pathPrefix stringByAppendingPathComponent: [cell title]]]; } return array; } /** * Returns a path to the category which contains the current selection. */ - (NSString *) containingCategory { NSArray * selectedCells; selectedCells = [browser selectedCells]; if ([selectedCells count] > 1) { return [browser pathToColumn: [browser selectedColumn]]; } else if ([selectedCells count] == 1) { if ([[selectedCells objectAtIndex: 0] isLeaf]) { return [browser pathToColumn: [browser selectedColumn]]; } else { return [browser path]; } } else { return [browser path]; } } /** * Instructs the file manager to perform a drag operation. The drag * operation is specified by `sender'. The operation source is fully * specified by the `sender' argument, the destination is the current * file browser path. * * @return YES if the drag operation succeeds, NO otherwise. */ - (BOOL) performDragOperation: (id ) sender { NSString * destCategory = [self containingCategory]; NSPasteboard * pb; int operation; NSUserDefaults * df = [NSUserDefaults standardUserDefaults]; BOOL ask; NSDictionary * projectFilesData; if ([sender draggingSourceOperationMask] & NSDragOperationMove) { operation = NSDragOperationMove; ask = ![df boolForKey: @"DontAskWhenMoving"]; } else if ([sender draggingSourceOperationMask] & NSDragOperationLink) { operation = NSDragOperationLink; ask = ![df boolForKey: @"DontAskWhenLinking"]; } else { operation = NSDragOperationCopy; ask = ![df boolForKey: @"DontAskWhenCopying"]; } pb = [sender draggingPasteboard]; // is it one of our project files? projectFilesData = [pb propertyListForType: ProjectFilesPboardType]; if (projectFilesData != nil && [[projectFilesData objectForKey: @"Project"] isEqualToString: [document fileName]]) { NSArray * filenames; NSEnumerator * e; NSString * filename; if (ask) { NSString * title, * message; switch (operation) { case NSDragOperationMove: title = _(@"Really move files?"); message = _(@"Really move the selected files to category %@?"); break; case NSDragOperationLink: title = _(@"Really link files?"); message = _(@"Really link the selected files from category %@?"); break; default: title = _(@"Really copy files?"); message = _(@"Really copy the selected files to category %@?"); break; } if (NSRunAlertPanel(title, message, _(@"Yes"), _(@"Cancel"), nil, [destCategory lastPathComponent]) != NSAlertDefaultReturn) { return NO; } } filenames = [projectFilesData objectForKey: @"Filenames"]; e = [filenames objectEnumerator]; while ((filename = [e nextObject]) != nil) { NSError * error; NSString * source = filename, * destination = [destCategory stringByAppendingPathComponent: [filename lastPathComponent]]; switch (operation) { case NSDragOperationMove: if (![self movePath: source toPath: destination error: &error]) { DescribeError (error, _(@"Error moving file"), _(@"Couldn't move file %@ to %@"), nil, nil, nil, [filename lastPathComponent], [destCategory lastPathComponent]); return NO; } break; case NSDragOperationLink: if (![self linkPath: source fromPath: destination error: &error]) { DescribeError (error, _(@"Error linking file"), _(@"Couldn't link file %@ from %@"), nil, nil, nil, [filename lastPathComponent], [destCategory lastPathComponent]); return NO; } break; default: if (![self copyPath: source toPath: destination error: &error]) { DescribeError (error, _(@"Error copying file"), _(@"Couldn't copy file %@ to %@"), nil, nil, nil, [filename lastPathComponent], [destCategory lastPathComponent]); return NO; } break; } } } // no, the file originates from somewhere outside - import it else { NSEnumerator * e; NSString * filepath; if (ask) { if (NSRunAlertPanel(_(@"Really import files?"), _(@"Really copy the selected files into the project?"), _(@"Yes"), _(@"Cancel"), nil) != NSAlertDefaultReturn) { return NO; } } e = [[pb propertyListForType: NSFilenamesPboardType] objectEnumerator]; while ((filepath = [e nextObject]) != nil) { NSError * error; /* N.B. when importing we ignore the NSDragOperationMove * possibility - we always only copy or link the files. * This is for safety reasons: in case the user dragged * a file from the file viewer and forgot to hit `Command' * we won't delete the original files. */ if (![self importFile: filepath toPath: destCategory link: operation == NSDragOperationLink error: &error]) { DescribeError (error, _(@"Error importing file"), _(@"Couldn't import file %@"), nil, nil, nil, filepath); return NO; } } } return YES; } /** * Opens the file at `aPath'. This method is invoked when the user requests * to open a file, but the delegate responded that it cannot handle that * open request. * * @return YES if the open operation succeeds, NO otherwise. */ - (BOOL) openPath: (NSString *) aPath { NSString * diskPath; NSString * fileType; NSString * app; NSWorkspace * ws = [NSWorkspace sharedWorkspace]; fileType = [self internalTypeOfFileAtPath: aPath]; if ([fileType isEqualToString: ProjectFileTypeLink]) { diskPath = [self targetOfLinkAtPath: aPath]; } else { BOOL isCategory = [fileType isEqualToString: ProjectFileTypeCategory]; diskPath = [delegate pathToFile: aPath isCategory: isCategory]; } app = [ws getBestAppInRole: nil forExtension: [diskPath pathExtension]]; if (app != nil) { return [ws openFile: diskPath]; } else { fileType = [[[NSFileManager defaultManager] fileAttributesAtPath: diskPath traverseLink: YES] fileType]; if ([fileType isEqualToString: NSFileTypeRegular]) { if (CheckTextFile (diskPath)) { return [document openFile: diskPath inCodeEditorOnLine: -1]; } else { return NO; } } else { return NO; } } } /** * Determines whether a file exists at `aPath'. * * @return YES if the file exists, otherwise NO. */ - (BOOL) fileExistsAtPath: (NSString *) aPath { return ([self fileEntryAtPath: aPath] != nil); } /** * Lists the files contained in the category at path `category'. * * @return An array of filenames of files contained in the category, * or `nil' if `category' doesn't exist or isn't a category file type. */ - (NSArray *) filesAtPath: (NSString *) category { return [[self categoryContentsArrayAtPath: category] valueForKey: @"Name"]; } /** * Queries the file type at path `aPath'. * * @return The file's type if the file is found, or -1 if it isn't. */ - (FMFileType) typeOfFileAtPath: (NSString *) aPath { return ExternalFileTypeFromInternal ([self internalTypeOfFileAtPath: aPath]); } /** * Queries the target of the link at path `aPath'. * * @return The link's target, or `nil' if the file at path `aPath' * doesn't exist, or isn't a link. */ - (NSString *) targetOfLinkAtPath: (NSString *) aPath { return [[self fileEntryAtPath: aPath] objectForKey: @"Target"]; } /** * Measures the disk usage of files at and under path `aPath'. * * @return The disk usage in bytes. */ - (unsigned long long) measureDiskUsageAtPath: (NSString *) aPath { NSString * fileType = [self internalTypeOfFileAtPath: aPath]; if ([fileType isEqualToString: ProjectFileTypePlain]) { unsigned long long value; NSFileManager * fm = [NSFileManager defaultManager]; NSString * realPath = [delegate pathToFile: aPath isCategory: NO]; NSDictionary * fattrs = [fm fileAttributesAtPath: realPath traverseLink: NO]; if ([[fattrs fileType] isEqualToString: NSFileTypeDirectory]) { NSDirectoryEnumerator * de = [fm enumeratorAtPath: realPath]; for (value = 0; [de nextObject] != nil; value += [[de fileAttributes] fileSize]); } else { value = [fattrs fileSize]; } return value; } else if ([fileType isEqualToString: ProjectFileTypeLink]) { #ifdef HAVE_SYMLINKS return [[[NSFileManager defaultManager] fileAttributesAtPath: [delegate pathToFile: aPath isCatgory: NO] traverseLink: NO] fileSize]; #else return 0; #endif } else if ([fileType isEqualToString: ProjectFileTypeCategory]) { unsigned long long value = 0; NSEnumerator * e = [[self filesAtPath: aPath] objectEnumerator]; NSString * filename; while ((filename = [e nextObject]) != nil) { value += [self measureDiskUsageAtPath: [aPath stringByAppendingPathComponent: filename]]; } return value; } else if ([fileType isEqualToString: ProjectFileTypeVirtual]) { return 0; } [NSException raise: NSInternalInconsistencyException format: _(@"Unknown file type %@ of file %@ found."), [self internalTypeOfFileAtPath: aPath], aPath]; return -1; } /** * Attempts to locate a file in the project based on it's physical * disk location. * * This method searches the project's categories for a file which * exists at the specified on-disk location and returns the path * to it in the project. * * @param diskLocation The physical location of the file. * * @return The location in the project where the file is registered, * or `nil' if no such file exists. */ - (NSString *) pathToFileAtPhysicalPath: (NSString *) diskLocation { return [self recursivelyLocateFileAtPhysicalPath: diskLocation inCategory: @"/"]; } /** * Returns a list of files of a specified type in a category. This method * looks for the specified file type only, and also allows to specify * whether the lookup should be recursive. */ - (NSArray *) filesAtPath: (NSString *) aCategory ofType: (FMFileType) aFileType recursive: (BOOL) recursive { NSMutableArray * array = [NSMutableArray array]; LocateFilesOfType ([self categoryContentsArrayAtPath: aCategory], array, InternalFileTypeFromExternal (aFileType), recursive); return [[array copy] autorelease]; } /** * A shorthand for -[FileManager importFile:renameTo:toPath:link:error:] * with the rename filename being the same as the original file path. */ - (BOOL) importFile: (NSString *) filePath toPath: (NSString *) category link: (BOOL) linkFlag error: (NSError **) error { return [self importFile: filePath renameTo: [filePath lastPathComponent] toPath: category link: linkFlag error: error]; } /** * Imports a specified on-disk file into the project. * * @param filePath The on-disk file which to import into the project. * @param newName A filename (only the last path component) to which * the imported file will be renamed in the project. * @param category The category into which to import the file. * @param linkFlag If set to NO, the file, if located in an unsuitable * location outside the project, will be copied into the path. * If YES is passed, it will be linked to without copying. * @param error A pointer to a location which will be set to point to * an NSError object in case an error arises during the operation. * * @return YES if the import succeeds, NO if it isn't. */ - (BOOL) importFile: (NSString *) filePath renameTo: (NSString *) newName toPath: (NSString *) category link: (BOOL) linkFlag error: (NSError **) error { FileImportResult result = [delegate importFile: filePath intoCategory: category error: error]; switch (result) { case FileImportCannotHandle: return [self internalImportFile: filePath renameTo: newName toPath: category link: linkFlag error: error]; case FileImportFailure: return NO; case FileImportSuccess: return YES; } [NSException raise: NSInternalInconsistencyException format: @"FileManager: delegate %@ replied with " @"invalid reply %i to -importFile:intoCategory:", delegate, result]; return NO; } /** * Creates an empty category named `categoryName' in category `category'. * * @return YES if the operation succeeds, NO otherwise. */ - (BOOL) createCategory: (NSString *) categoryName atPath: (NSString *) category error: (NSError **) error { NSString * destPath = [category stringByAppendingPathComponent: categoryName]; if ([self fileExistsAtPath: destPath]) { SetFileError (error, ProjectFilesAlreadyExistError, _(@"A category named %@ already exists in the project in %@."), categoryName, category); return NO; } [self addEntryAtPath: destPath ofType: ProjectFileTypeCategory withArgument: nil]; [document updateChangeCount: NSChangeDone]; PostFilesChangedNotification (self, category); return YES; } /** * If necessary, creates a category and all intermediate category nodes * on the way to it. * * @param category The category which to create. * @param error A pointer to location which will be set to an NSError * object in case an error occurs. * * @return YES if the operation succeeds, NO if it doesn't. */ - (BOOL) createCategoryAndIntermediateCategories: (NSString *) category error: (NSError **) error { NSString * path = @"/"; NSEnumerator * e = [[category pathComponents] objectEnumerator]; NSString * pathComponent; // skip the '/' path component [e nextObject]; while ((pathComponent = [e nextObject]) != nil) { NSString * fileType; path = [path stringByAppendingPathComponent: pathComponent]; fileType = [self internalTypeOfFileAtPath: path]; if (fileType == nil) { if (![self createCategory: [path lastPathComponent] atPath: [path stringByDeletingLastPathComponent] error: error]) { return NO; } } else if (![fileType isEqualToString: ProjectFileTypeCategory]) { SetFileError (error, ProjectFilesInvalidFileTypeError, _(@"Error creating category %@, a file is in the way"), path); } } return YES; } /** * Creates a virtual file named `filename' in category `category'. * * @return YES if the operation succeeds, NO otherwise. */ - (BOOL) createVirtualFileNamed: (NSString *) filename atPath: (NSString *) category error: (NSError **) error { NSString * destPath = [category stringByAppendingPathComponent: filename]; if ([self fileExistsAtPath: destPath]) { SetFileError (error, ProjectFilesAlreadyExistError, _(@"A file named %@ already exists in the project in %@."), filename, category); return NO; } [self addEntryAtPath: destPath ofType: ProjectFileTypeVirtual withArgument: nil]; [document updateChangeCount: NSChangeDone]; PostFilesChangedNotification (self, category); return YES; } /** * Removes the path `aPath', deleting any underlying disk files * if `deleteFlag' = YES. * * @return YES if the operation succeeds, NO otherwise. */ - (BOOL) removePath: (NSString *) aPath delete: (BOOL) deleteFlag error: (NSError **) error { NSFileManager * fm = [NSFileManager defaultManager]; NSString * fileType = [self internalTypeOfFileAtPath: aPath]; #ifdef HAVE_SYMLINKS // on system with symlink support, delete the symlink file as well if ([fileType isEqualToString: ProjectFileTypePlain] || [fileType isEqualToString: ProjectFileTypeLink]) #else if ([fileType isEqualToString: ProjectFileTypePlain]) #endif { if (deleteFlag) { NSString * filePath = [delegate pathToFile: aPath isCategory: NO]; // unlink the file if (![fm removeFileAtPath: filePath handler: nil]) { SetFileError (error, ProjectFilesDeletionError, _(@"Unable to remove disk file at path %@."), filePath); return NO; } else { // reduce the project's directory structure as far // as possible if (!PurgeUnneededDirectories([filePath stringByDeletingLastPathComponent], error)) { return NO; } } } } // recursively traverse category contents and remove them first else if ([fileType isEqualToString: ProjectFileTypeCategory]) { NSEnumerator * e = [[self filesAtPath: aPath] objectEnumerator]; NSString * filename; while ((filename = [e nextObject]) != nil) { if (![self removePath: [aPath stringByAppendingPathComponent: filename] delete: deleteFlag error: error]) { return NO; } } } [self removeEntryAtPath: aPath]; [document updateChangeCount: NSChangeDone]; PostFilesChangedNotification (self, [aPath stringByDeletingLastPathComponent]); return YES; } /** * Copies a specified file to a new location. * * @return YES if the operation succeeds, NO otherwise. */ - (BOOL) copyPath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error { NSFileManager * fm = [NSFileManager defaultManager]; NSString * fileType; NSString * srcPath, * destPath; BOOL isCategory; if ([aPath isEqualToString: newPath]) { return YES; } if (![self performFileClashCheckFromPath: aPath toPath: newPath error: error]) { return NO; } fileType = [self internalTypeOfFileAtPath: aPath]; isCategory = [fileType isEqualToString: ProjectFileTypeCategory]; srcPath = [delegate pathToFile: aPath isCategory: isCategory]; destPath = [delegate pathToFile: newPath isCategory: isCategory]; if ([srcPath isEqualToString: destPath]) { SetFileError (error, ProjectFilesAlreadyExistError, _(@"The source and destination files reside " @"in the same on-disk directory.")); return NO; } if ([fileType isEqualToString: ProjectFileTypePlain]) { if (![self copyPlainFileAtPath: aPath toPath: newPath error: error]) { return NO; } } else if ([fileType isEqualToString: ProjectFileTypeLink]) { if (![self copyLinkAtPath: aPath toPath: newPath error: error]) { return NO; } } else if ([fileType isEqualToString: ProjectFileTypeCategory]) { if (![self copyCategoryAtPath: aPath toPath: newPath error: error]) { return NO; } } else if ([fileType isEqualToString: ProjectFileTypeVirtual]) { [self addEntryAtPath: newPath ofType: ProjectFileTypeVirtual withArgument: nil]; } [document updateChangeCount: NSChangeDone]; PostFilesChangedNotification (self, [newPath stringByDeletingLastPathComponent]); return YES; } /** * Moves a specified file to a new location. * * @param aPath The path from which to move the file. * @param newPath The path to which to move the file. * @param error A pointer to a location which will be filled with * an error description in case the operation fails. * * @return YES if the operation succeeds, NO otherwise. */ - (BOOL) movePath: (NSString *) aPath toPath: (NSString *) newPath error: (NSError **) error { NSString * fileType; if ([aPath isEqualToString: newPath]) { return YES; } if (![self performFileClashCheckFromPath: aPath toPath: newPath error: error]) { return NO; } fileType = [self internalTypeOfFileAtPath: aPath]; if ([fileType isEqualToString: ProjectFileTypePlain]) { if (![self movePlainFileAtPath: aPath toPath: newPath error: error]) { return NO; } } else if ([fileType isEqualToString: ProjectFileTypeLink]) { if (![self moveLinkAtPath: aPath toPath: newPath error: error]) { return NO; } } else if ([fileType isEqualToString: ProjectFileTypeCategory]) { if (![self moveCategoryAtPath: aPath toPath: newPath error: error]) { return NO; } } else if ([fileType isEqualToString: ProjectFileTypeVirtual]) { [self addEntryAtPath: newPath ofType: ProjectFileTypeVirtual withArgument: nil]; } [document updateChangeCount: NSChangeDone]; PostFilesChangedNotification (self, [aPath stringByDeletingLastPathComponent]); PostFilesChangedNotification (self, [newPath stringByDeletingLastPathComponent]); return YES; } /** * Links a specified file from a new location. Only links to plain * files and other links are supported. * * @param aPath The path to which to link. * @param newPath The path where to create the link. * * @return YES if the operation succeeds, NO if it doesn't. */ - (BOOL) linkPath: (NSString *) aPath fromPath: (NSString *) newPath error: (NSError **) error { NSString * linkTarget; NSString * fileType; NSFileManager * fm = [NSFileManager defaultManager]; NSString * srcPath = [delegate pathToFile: newPath isCategory: NO], * destPath = [delegate pathToFile: aPath isCategory: NO]; // there already is such a file if ([self fileExistsAtPath: newPath]) { SetFileError (error, ProjectFilesAlreadyExistError, _(@"File %@ already exists"), aPath); return NO; } // on-disk file in the way if ([fm fileExistsAtPath: srcPath]) { SetFileError (error, ProjectFilesAlreadyExistError, _(@"An on-disk file at %@ is in the way."), srcPath); return NO; } fileType = [self internalTypeOfFileAtPath: aPath]; if ([fileType isEqualToString: ProjectFileTypePlain]) { linkTarget = destPath; } else if ([fileType isEqualToString: ProjectFileTypeLink]) { linkTarget = [self targetOfLinkAtPath: aPath]; } else { SetFileError (error, ProjectFilesInvalidFileTypeError, _(@"Invalid file type of file %@. Can only link to plain " @"files and other links."), aPath); return NO; } // construct a new relative path linkTarget = TranslocateLinkTarget(linkTarget, srcPath, destPath); linkTarget = [destPath stringByConstructingRelativePathTo: linkTarget]; #ifdef HAVE_SYMLINKS if (!CreateDirectoryAndIntermediateDirectories([destPath stringByDeletingLastPathComponent], error)) if (![fm createSymbolicLinkAtPath: destPath pathContent: linkTarget]) { SetFileError (error, ProjectFilesCreationError, _(@"Couldn't create a symbolic link on disk at %@."), destPath); return NO; } #endif [self addEntryAtPath: newPath ofType: ProjectFileTypeLink withArgument: linkTarget]; [document updateChangeCount: NSChangeDone]; PostFilesChangedNotification (self, [newPath stringByDeletingLastPathComponent]); return YES; } /** * Returns an iconic representation of `aPath'. */ - (NSImage *) iconForPath: (NSString *) aPath { NSImage * icon; icon = [delegate iconForPath: aPath]; if (icon != nil) { return icon; } else { NSWorkspace * ws = [NSWorkspace sharedWorkspace]; NSString * fileType; fileType = [self internalTypeOfFileAtPath: aPath]; if ([fileType isEqualToString: ProjectFileTypePlain]) { return [ws iconForFile: [delegate pathToFile: aPath isCategory: NO]]; } else if ([fileType isEqualToString: ProjectFileTypeLink]) { return [ws iconForFile: [[delegate pathToFile: aPath isCategory: NO] stringByConcatenatingWithPath: [self targetOfLinkAtPath: aPath]]]; } else if ([fileType isEqualToString: ProjectFileTypeCategory]) { return [NSImage imageNamed: @"ProjectCategory" owner: self]; } else if ([fileType isEqualToString: ProjectFileTypeVirtual]) { return [ws iconForFileType: [aPath pathExtension]]; } return nil; } } - (void) importFiles: sender { if (![self validateAction: @selector (importFiles:)]) { return; } NSString * category = [self containingCategory]; NSOpenPanel * op = [NSOpenPanel openPanel]; static NSButton * linkButton = nil; if (linkButton == nil) { linkButton = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 100, 18)]; [linkButton setButtonType: NSSwitchButton]; [linkButton setTitle: _(@"Link Files")]; [linkButton sizeToFit]; } [linkButton setState: NO]; [op setTitle: _(@"Choose file(s) to add to the project")]; [op setAllowsMultipleSelection: YES]; [op setAccessoryView: linkButton]; [op setCanChooseFiles: YES]; [op setCanChooseDirectories: YES]; if ([op runModalForTypes: [delegate permissibleFileExtensionsInCategory: category]] == NSOKButton) { BOOL createLink = [linkButton state]; NSEnumerator * e = [[op filenames] objectEnumerator]; NSString * filename; NSError * error; while ((filename = [e nextObject]) != nil) { if (![self importFile: filename toPath: category link: createLink error: &error]) { if (NSRunAlertPanel(_(@"Error importing file"), _(@"%@\nContinue import of other file(s)?"), _(@"Yes"), _(@"No"), nil, [[error userInfo] objectForKey: NSLocalizedDescriptionKey]) != NSAlertDefaultReturn) { break; } } } } [op setAccessoryView: nil]; } - (void) newEmptyFile: sender { if (![self validateAction: @selector (newEmptyFile:)]) { return; } NSString * selectedCategory = [self containingCategory]; NSString * newFileName = [self makeNewUniqueNameFromBasename: _(@"New File") pathExtension: nil inCategory: selectedCategory andDirectory: [delegate pathToFile: selectedCategory isCategory: YES]]; NSString * filePath = [delegate pathToFile: [selectedCategory stringByAppendingPathComponent: newFileName] isCategory: NO]; NSError * error; if (!CreateDirectoryAndIntermediateDirectories([filePath stringByDeletingLastPathComponent], &error)) { NSRunAlertPanel(_(@"Can't create new file"), _(@"Couldn't create a new directory for the file in the project.\n" @" Perhaps you don't have write permissions for the project.\n%@."), nil, nil, nil, [[error userInfo] objectForKey: NSLocalizedDescriptionKey]); return; } if (![[NSFileManager defaultManager] createFileAtPath: filePath contents: nil attributes: nil]) { NSRunAlertPanel(_(@"Can't create new file"), _(@"Couldn't create a new file in the project. Perhaps\n" @"you don't have write permissions for the project."), nil, nil, nil); return; } if (![self importFile: filePath toPath: selectedCategory link: NO error: &error]) { DescribeError (error, _(@"Error creating new file"), _(@"Couldn't create new file"), nil, nil, nil); } else { [self selectAndEditNameAtPath: [selectedCategory stringByAppendingPathComponent: newFileName]]; } } - (void) newFileFromTemplate: sender { if (![self validateAction: @selector (newFileFromTemplate:)]) { return; } TemplateFileSelector * tfs = [TemplateFileSelector shared]; NSString * category = [self containingCategory]; NSString * templatesDirectory = [delegate pathToFileTemplatesDirectoryForCategory: category]; if ([tfs runModalForTemplatesDirectory: templatesDirectory] == NSOKButton) { NSString * templateFile = [tfs templateFile]; NSString * filename = [tfs filename]; NSError * error; // strip the path extension - we will afterwards selectively append // it as appropriate filename = [filename stringByDeletingPathExtension]; if (![self importFile: templateFile renameTo: [filename stringByAppendingPathExtension: [templateFile pathExtension]] toPath: category link: NO error: &error]) { DescribeError (error, _(@"Error importing file"), _(@"Couldn't import the template file"), nil, nil, nil); return; } if ([tfs shouldImportAssociatedFiles]) { NSDictionary * associatedFiles = [delegate filesAssociatedWithTemplateFile: templateFile fromTemplatesDirectory: templatesDirectory forCategory: category]; NSEnumerator * e = [associatedFiles keyEnumerator]; NSString * associatedTemplateFile; while ((associatedTemplateFile = [e nextObject]) != nil) { NSString * destCategory = [associatedFiles objectForKey: associatedTemplateFile]; if (![self createCategoryAndIntermediateCategories: destCategory error: &error] || ![self importFile: associatedTemplateFile renameTo: [filename stringByAppendingPathExtension: [associatedTemplateFile pathExtension]] toPath: destCategory link: NO error: &error]) { if (DescribeError (error, _(@"Error importing file"), _(@"Couldn't import associated template file"), _(@"Continue import"), _(@"Abort import"), nil) == NSAlertDefaultReturn) { continue; } else { return; } } } } } } - (void) newCategory: sender { if (![self validateAction: @selector (newCategory:)]) { return; } NSError * error; NSString * selectedCategory; NSString * newCategoryName; if ([[browser selectedCell] isLeaf] || [[browser selectedCells] count] > 1) { selectedCategory = [self containingCategory]; } else { selectedCategory = [browser path]; } newCategoryName = [self makeNewUniqueNameFromBasename: _(@"New Category") pathExtension: nil inCategory: selectedCategory andDirectory: nil]; if ([self createCategory: newCategoryName atPath: selectedCategory error: &error]) { [self selectAndEditNameAtPath: [selectedCategory stringByAppendingPathComponent: newCategoryName]]; } else { DescribeError (error, _(@"Can't create category"), _(@"Couldn't create category"), nil, nil, nil); } } - (void) deleteFiles: sender { if (![self validateAction: @selector (deleteFiles:)]) { return; } NSArray * filenames = [self selectedFiles]; NSString * category = [self containingCategory]; BOOL delete = NO; NSEnumerator * e = [filenames objectEnumerator]; NSString * filename; NSError * error; switch (NSRunAlertPanel(_(@"Delete file(s)?"), _(@"Do you really want to delete the selected file(s) from category %@?"), _(@"Yes, from project AND disk"), _(@"Yes, from project only"), _(@"Cancel"), category)) { case NSAlertDefaultReturn: delete = YES; break; case NSAlertOtherReturn: return; } while ((filename = [e nextObject]) != nil) { if (![self removePath: filename delete: delete error: &error]) { DescribeError (error, _(@"Error deleting file"), _(@"Couldn't delete file %@"), nil, nil, nil, [filename lastPathComponent]); return; } } } - (void) filesChanged: (NSNotification *) notif { NSString * category = [[notif userInfo] objectForKey: @"Category"]; NSString * browserPath = [browser path]; int numCategoryComponents = [[category pathComponents] count], numBrowserPathComponents = [[browserPath pathComponents] count]; // If the category which changed was some super-category of the currently // displayed one (the path is shorter), reload it. // Do it also if the browser path is equal to the category which // changed. if (numCategoryComponents < numBrowserPathComponents || [category isEqualToString: browserPath]) { [browser reloadColumn: numCategoryComponents - 1]; [browser setPath: browserPath]; [self selectFile: self]; } } - (void) projectNameChanged: (NSNotification *) notif { if ([[browser path] isEqualToString: @"/"]) { [fileNameField setStringValue: [document projectName]]; } } - (NSArray *) moduleMenuItems { return [NSArray arrayWithObjects: PMMakeMenuItem (_(@"Import Files..."), @selector(importFiles:), nil, self), PMMakeMenuItem (_(@"New Empty File"), @selector(newEmptyFile:), nil, self), PMMakeMenuItem (_(@"New File From Template..."), @selector(newFileFromTemplate:), nil, self), PMMakeMenuItem (_(@"New Category"), @selector(newCategory:), nil, self), PMMakeMenuItem (_(@"Delete Files..."), @selector(deleteFiles:), nil, self), nil]; } - (NSArray *) toolbarItemIdentifiers { return [NSArray arrayWithObjects: @"FileManagerImportFilesItemIdentifier", @"FileManagerNewEmptyFileItemIdentifier", @"FileManagerNewFileFromTemplateItemIdentifier", @"FileManagerNewCategoryItemIdentifier", @"FileManagerDeleteFilesItemIdentifier", nil]; } - (NSToolbarItem *) toolbarItemForItemIdentifier: (NSString *) itemIdentifier { NSToolbarItem * toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier: itemIdentifier] autorelease]; NSMenuItem * menuItem = [NSMenuItem alloc]; if ([itemIdentifier isEqualToString: @"FileManagerImportFilesItemIdentifier"]) { [toolbarItem setAction: @selector (importFiles:)]; [toolbarItem setLabel: _(@"Import Files...")]; [toolbarItem setImage: [NSImage imageNamed: @"ImportFiles" owner: self]]; [toolbarItem setToolTip: _(@"Imports external files into the " @"selected category")]; menuItem = [menuItem initWithTitle: _(@"Import Files...") action: @selector (importFiles:) keyEquivalent: nil]; } else if ([itemIdentifier isEqualToString: @"FileManagerNewEmptyFileItemIdentifier"]) { [toolbarItem setAction: @selector (newEmptyFile:)]; [toolbarItem setLabel: _(@"New Empty File")]; [toolbarItem setImage: [NSImage imageNamed: @"NewEmptyFile" owner: self]]; [toolbarItem setToolTip: _(@"Creates an empty file in the selected " @"category")]; menuItem = [menuItem initWithTitle: _(@"New Empty File") action: @selector (newEmptyFile:) keyEquivalent: nil]; } else if ([itemIdentifier isEqualToString: @"FileManagerNewFileFromTemplateItemIdentifier"]) { [toolbarItem setAction: @selector (newFileFromTemplate:)]; [toolbarItem setLabel: _(@"New File From Template...")]; [toolbarItem setImage: [NSImage imageNamed: @"NewFileFromTemplate" owner: self]]; [toolbarItem setToolTip: _(@"Imports a template file into the " @"selected category")]; menuItem = [menuItem initWithTitle: _(@"New File From Template...") action: @selector (newFileFromTemplate:) keyEquivalent: nil]; } else if ([itemIdentifier isEqualToString: @"FileManagerNewCategoryItemIdentifier"]) { [toolbarItem setAction: @selector (newCategory:)]; [toolbarItem setLabel: _(@"New Category")]; [toolbarItem setImage: [NSImage imageNamed: @"NewCategory" owner: self]]; [toolbarItem setToolTip: _(@"Creates a new subcategory in the " @"selected category")]; menuItem = [menuItem initWithTitle: _(@"New Category") action: @selector (newCategory:) keyEquivalent: nil]; } else if ([itemIdentifier isEqualToString: @"FileManagerDeleteFilesItemIdentifier"]) { [toolbarItem setAction: @selector (deleteFiles:)]; [toolbarItem setLabel: _(@"Delete Files...")]; [toolbarItem setImage: [NSImage imageNamed: @"DeleteFiles" owner: self]]; [toolbarItem setToolTip: _(@"Deletes the selected file(s) and " @"category(ies)")]; menuItem = [menuItem initWithTitle: _(@"Delete Files...") action: @selector (deleteFiles:) keyEquivalent: nil]; } else { // not one of our items - skip further initialization steps, // release the menu item and return the toolbar item // this is just to make sure that releasing the menu item will release // it when fully initialized, so that some implementation-specific // bug won't cause problems here menuItem = [menuItem initWithTitle: nil action: NULL keyEquivalent: nil]; DESTROY (menuItem); return toolbarItem; } [menuItem setTarget: self]; [menuItem autorelease]; [toolbarItem setTarget: self]; [toolbarItem setMenuFormRepresentation: menuItem]; return toolbarItem; } - (BOOL) validateMenuItem: (id ) menuItem { return [self validateAction: [menuItem action]]; } - (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem { return [self validateAction: [toolbarItem action]]; } - (BOOL) regenerateDerivedFiles { return YES; } @end