/* ProjectCreator.m 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 "ProjectCreator.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "ProjectTypeLoader.h" #import "ProjectType.h" #import "ProjectTypeDescription.h" #import "ProjectTemplateDescription.h" #import "Preferences.h" #import "ProjectDocument.h" NSString * const ProjectCreatorErrorDomain = @"ProjectCreatorErrorDomain"; @implementation ProjectCreator static ProjectCreator * shared = nil; + shared { if (shared == nil) { shared = [self new]; } return shared; } /** * Creates a new project. * * @param projectPath The path where to create the project. * @param projectName The name of the new project. * @param templatePath A path to the template which will be used * to create the new project's files. * @param error A pointer which if not set to NULL will be filled * with an error object describing the problem in case creating * the project fails. * * After the template has been copied, the project directory is * searched for files named "$PROJECT_NAME$". and these are * then renamed to the proper project name. Simmilar variable * substitution occurs inside the ProjectInfo.plist. * * @return YES if creating the new project bundle succeeded, NO if it didn't. */ + (BOOL) createNewProjectAtPath: (NSString *) aProjectPath projectName: (NSString *) aProjectName fromTemplate: (NSString *) aTemplatePath error: (NSError **) error { NSFileManager * fm = [NSFileManager defaultManager]; NSDirectoryEnumerator * de; NSString * filename; if (!ImportProjectFile(aTemplatePath, aProjectPath, aProjectName, error)) { return NO; } // Now read in the project's ".pmproj" file and replace variables in it. { NSString * projectInfoPlistPath; NSString * projectInfoPlist; projectInfoPlistPath = [aProjectPath stringByAppendingPathComponent: [aProjectName stringByAppendingPathExtension: @"pmproj"]]; if (!ImportProjectFile(projectInfoPlistPath, projectInfoPlistPath, aProjectName, error)) { NSLog(_(@"Failed to create new project at path: %@"), aProjectPath); return NO; } } return YES; } - (void) awakeFromNib { [projectNameView retain]; [projectNameView removeFromSuperview]; [projectTypeView retain]; [projectTypeView removeFromSuperview]; DESTROY(window1); DESTROY(window2); projectNameCell = [projectNameForm cellAtIndex: 0]; [[projectNameNotice enclosingScrollView] setHasVerticalScroller: NO]; [[projectNameMistake enclosingScrollView] setHasVerticalScroller: NO]; [[projectTypeDescription enclosingScrollView] setHasVerticalScroller: NO]; [projectNameMistake setTextColor: [NSColor redColor]]; [projectNameNotice setTextColor: [NSColor disabledControlTextColor]]; [projectTypes setDoubleAction: @selector(doubleClickedProjectType:)]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(validateProjectName:) name: NSControlTextDidChangeNotification object: projectNameForm]; } - (void) dealloc { TEST_RELEASE(wizard); TEST_RELEASE(projectTypesCache); TEST_RELEASE(location); TEST_RELEASE(projectName); [super dealloc]; } - init { if ((self = [super init]) != nil) { NSArray * classes = [[[ProjectTypeLoader shared] projectTypes] allValues]; NSEnumerator * e = [classes objectEnumerator]; Class projType; NSMutableArray * array = [NSMutableArray arrayWithCapacity: [classes count]]; while ((projType = [e nextObject]) != nil) { [array addObject: [[[ProjectTypeDescription alloc] initWithProjectType: projType] autorelease]]; } ASSIGNCOPY(projectTypesCache, array); } return self; } /** * Runs a series of panels to ask the user to define a new project. * * @param withLocation If this argument is NO, then the user isn't * asked about the location of the project (and the key `ProjectPath' * will be absent in the return value), and the calling code is assumed * to know where the project is supposed to reside. * * @return A dictionary with these key-value pairs: * { * ProjectName = projectName; * ProjectPath = projectPath; * ProjectType = projectTypeID; * ProjectTemplate = templateLocation; * } */ - (NSDictionary *) getNewProjectSetupWithLocation: (BOOL) withLocation { NSSavePanel * sp; ProjectTemplateDescription * templateDescription; ProjectTypeDescription * typeDescription; if (wizard == nil) { [NSBundle loadNibNamed: @"ProjectCreator" owner: self]; } [finishButton setEnabled: NO]; [backToLocationButton setEnabled: withLocation]; [backToLocationButton setTransparent: !withLocation]; [projectNameCell setStringValue: nil]; sp = [NSSavePanel savePanel]; [sp setTitle: _(@"Please choose a project location")]; [sp setCanCreateDirectories: YES]; if (withLocation == YES) { BOOL isDir; selectLocation: if ([sp runModal] == NSCancelButton) { return nil; } ASSIGN(location, [sp filename]); if ([[NSFileManager defaultManager] fileExistsAtPath: location isDirectory: &isDir]) { if (isDir) { if (NSRunAlertPanel(_(@"Use existing directory?"), _(@"You have selected an existing directory. The project\n" @"files will be created in %@.\n" @"Are you sure this is what you want?"), _(@"Yes, next"), _(@"No, back"), nil, location) == NSAlertAlternateReturn) { goto selectLocation; } } else { NSRunAlertPanel(_(@"Invalid path"), _(@"Cannot overwrite a file. Please choose a directory " @"or specify an unused filename."), nil, nil, nil); goto selectLocation; } } [projectNameNotice setString: _(@"Leave blank to make the " @"project name the same as the name of the project directory")]; } else { DESTROY(location); [projectNameNotice setString: _(@"You must specify a project name")]; } [self validateProjectName: nil]; switch ([wizard activate: nil]) { case NSRunStoppedResponse: break; case NSRunAbortedResponse: return nil; case -1: goto selectLocation; } templateDescription = [projectTypes itemAtRow: [projectTypes selectedRow]]; typeDescription = [templateDescription parent]; if (withLocation) { return [NSDictionary dictionaryWithObjectsAndKeys: projectName, @"ProjectName", [typeDescription name], @"ProjectType", [[typeDescription projectType] pathToProjectTemplate: [templateDescription name]] , @"ProjectTemplate", location, @"ProjectPath", nil]; } else { return [NSDictionary dictionaryWithObjectsAndKeys: projectName, @"ProjectName", [typeDescription name], @"ProjectType", [[typeDescription projectType] pathToProjectTemplate: [templateDescription name]] , @"ProjectTemplate", nil]; } } - (void) validateProjectName: sender { if ([[projectNameCell stringValue] length] == 0) { ASSIGN(projectName, [location lastPathComponent]); } else { ASSIGN(projectName, [projectNameCell stringValue]); } if (projectName != nil) { // validate a non-nil project name NSError * error = nil; [toProjectTypeSelectionButton setEnabled: [ProjectDocument validateProjectName: projectName error: &error]]; [projectNameMistake setString: [[error userInfo] objectForKey: NSLocalizedDescriptionKey]]; } else { // otherwise require the user to enter a project name [toProjectTypeSelectionButton setEnabled: NO]; [projectNameMistake setString: nil]; } } - (void) cancel: sender { [wizard deactivateWithCode: NSRunAbortedResponse]; } - (void) goToProjectLocationSelection: sender { [wizard deactivateWithCode: -1]; } - (void) projectTypeSelected: sender { int row = [projectTypes selectedRow]; if (row >= 0) { id object = [projectTypes itemAtRow: row]; if ([object isKindOfClass: [ProjectTemplateDescription class]]) { [finishButton setEnabled: YES]; [templateNeededNotice setStringValue: nil]; } else { [finishButton setEnabled: NO]; [templateNeededNotice setStringValue: _(@"Please select " @"a template")]; } [projectTypeIcon setImage: [object icon]]; [projectTypeDescription setString: [object description]]; } else { [finishButton setEnabled: NO]; [templateNeededNotice setStringValue: _(@"Please select " @"a project type")]; } } /** * Action invoked when the user double-clicks the projectType * outline. Since each double-click also invokes a single-click * before (which means that the user's choice has been validated * in -[ProjectCreator projectTypeSelected:], we only check whether * the button is enabled (meaning that the selection is valid) * and aftewards make the button perform a click. */ - (void) doubleClickedProjectType: sender { if ([finishButton isEnabled]) { [finishButton performClick: self]; } } - (NSView *) wizardPanel: (WKWizardPanel *) sender viewForStage: (NSString *) aStageName { if ([aStageName isEqualToString: @"Project Name"]) { return projectNameView; } else { return projectTypeView; } } - (NSView *) wizardPanel: (WKWizardPanel *) sender initialFirstResponderForStage: (NSString *) aStageName { if ([aStageName isEqualToString: @"Project Name"]) { return projectNameForm; } else { return projectTypes; } } - (int) outlineView: (NSOutlineView *) outlineView numberOfChildrenOfItem: (id) item { if (item == nil) { return [projectTypesCache count]; } else { if ([item isKindOfClass: [ProjectTypeDescription class]]) { return [[item templates] count]; } else { return 0; } } } - (BOOL) outlineView: (NSOutlineView *) outlineView isItemExpandable: (id) item { if ([item isKindOfClass: [ProjectTypeDescription class]]) { return YES; } else { return NO; } } - (id) outlineView: (NSOutlineView *) outlineView child: (int) index ofItem: (id) item { if (item == nil) { return [projectTypesCache objectAtIndex: index]; } else { return [[item templates] objectAtIndex: index]; } } - (id) outlineView: (NSOutlineView *) outlineView objectValueForTableColumn: (NSTableColumn *) tableColumn byItem: (id) item { return [item name]; } @end /** * Creates a directory at `dirPath' and the intermediate directories. * * This function creates a directory at `dirPath' (if it doesn't exist * already), and any intermediate directories as necessary (simmilar * to mkdir -p). If a file exists at the specified `dirPath' (or any of * intermediate directories is in fact a file), the operation fails. * * @return YES if the directory is created (or exists already), NO otherwise. */ BOOL CreateDirectoryAndIntermediateDirectories(NSString * dirPath, NSError ** error) { NSFileManager * fm = [NSFileManager defaultManager]; BOOL isDir; // does it already exist? if ([fm fileExistsAtPath: dirPath isDirectory: &isDir]) { if (isDir) { return YES; } // some file is in the way else { if (error != NULL) { NSDictionary * userInfo; userInfo = [NSDictionary dictionaryWithObject: [NSString stringWithFormat: _(@"Couldn't create intermediate " @"nodes to %@: a file is in the way."), dirPath] forKey: NSLocalizedDescriptionKey]; *error = [NSError errorWithDomain: ProjectCreatorErrorDomain code: ProjectDirectoryCreationError userInfo: userInfo]; } return NO; } } // no, start creating the intermediate directories as necessary else { NSEnumerator * e = [[dirPath pathComponents] objectEnumerator]; NSString * path, * pathComponent; for (path = [e nextObject]; (pathComponent = [e nextObject]) != nil;) { path = [path stringByAppendingPathComponent: pathComponent]; if ([fm fileExistsAtPath: path isDirectory: &isDir]) { if (isDir) { continue; } else { if (error != NULL) { NSDictionary * userInfo; userInfo = [NSDictionary dictionaryWithObject: [NSString stringWithFormat: _(@"Couldn't create intermediate " @"node %@: a file is in the way."), path] forKey: NSLocalizedDescriptionKey]; *error = [NSError errorWithDomain: ProjectCreatorErrorDomain code: ProjectDirectoryCreationError userInfo: userInfo]; } return NO; } } else { if (![fm createDirectoryAtPath: path attributes: nil]) { if (error != NULL) { NSDictionary * userInfo; userInfo = [NSDictionary dictionaryWithObject: [NSString stringWithFormat: _(@"Couldn't create intermediate directory %@"), path] forKey: NSLocalizedDescriptionKey]; *error = [NSError errorWithDomain: ProjectCreatorErrorDomain code: ProjectDirectoryCreationError userInfo: userInfo]; } return NO; } } } } return YES; } static BOOL ImportProjectDirectoryFile(NSString * sourcePath, NSString * destinationPath, NSString * projectName, NSError ** error) { NSFileManager * fm = [NSFileManager defaultManager]; NSString * filepath; NSDirectoryEnumerator * de; // do we need to copy at all? if (![sourcePath isEqualToString: destinationPath]) { if (!CreateDirectoryAndIntermediateDirectories([destinationPath stringByDeletingLastPathComponent], error)) { return NO; } if (![fm copyPath: sourcePath toPath: destinationPath handler: nil]) { if (error != NULL) { NSDictionary * userInfo; userInfo = [NSDictionary dictionaryWithObject: @"Failed to copy source directory " @"to destination" forKey: NSLocalizedDescriptionKey]; *error = [NSError errorWithDomain: ProjectCreatorErrorDomain code: ProjectFileImportError userInfo: userInfo]; } return NO; } } // enumerate through the files of the project and if we find // a file which has $PROJECT_NAME$ as part of it's name we // rename it to the proper project name performRenaming: de = [fm enumeratorAtPath: destinationPath]; while ((filepath = [de nextObject]) != nil) { NSString * origFilename = [filepath lastPathComponent]; NSMutableString * filename = [[origFilename mutableCopy] autorelease]; // substitute variables in the filename [filename replaceString: @"$PROJECT_NAME$" withString: projectName]; [filename replaceString: @"$HOSTNAME$" withString: [[NSProcessInfo processInfo] hostName]]; [filename replaceString: @"$USER$" withString: NSUserName()]; [filename replaceString: @"$DATE$" withString: [[NSDate date] description]]; // if the result is different from the original, perform // a move operation if (![filename isEqualToString: origFilename]) { NSDictionary * fattrs = [de fileAttributes]; NSString * newPath = [[filepath stringByDeletingLastPathComponent] stringByAppendingPathComponent: filename]; if (![fm movePath: [destinationPath stringByAppendingPathComponent: filepath] toPath: [destinationPath stringByAppendingPathComponent: newPath] handler: nil]) { if (error != NULL) { NSDictionary * userInfo; userInfo = [NSDictionary dictionaryWithObject: [NSString stringWithFormat: _(@"Failed to substitute %@ for %@"), newPath, filepath] forKey: NSLocalizedDescriptionKey]; *error = [NSError errorWithDomain: ProjectCreatorErrorDomain code: ProjectFileImportError userInfo: userInfo]; } return NO; } // if it's a directory we need to restart the search if ([[fattrs fileType] isEqualToString: NSFileTypeDirectory]) { goto performRenaming; } } } return YES; } static BOOL ImportProjectPlainFile(NSString * sourceFile, NSString * destinationFile, NSString * projectName, NSError ** error) { NSMutableString * string; string = [NSMutableString stringWithContentsOfFile: sourceFile]; if (string == nil) { if (error != NULL) { NSDictionary * userInfo; userInfo = [NSDictionary dictionaryWithObject: [NSString stringWithFormat: _(@"Couldn't import file %@: file not readable"), sourceFile] forKey: NSLocalizedDescriptionKey]; *error = [NSError errorWithDomain: ProjectCreatorErrorDomain code: ProjectFileImportError userInfo: userInfo]; } return NO; } // the following variables are expanded: // $PROJECT_NAME$ - expands to the project's name // $FILENAME$ - expands to the file's name, i.e. the last path // component of the path to the file // $FILENAME_EXT$ - expands to the file name's last path component // with it's path extension trimmed // $FILENAME_CAPS$ - expands to the file's name with all // letters uppercase. Additionally, all '.' will be replaced // with '_' // $FILENAME_LOWER$ - expands to the file's name with all // letters lowercase. Additionally, all '.' will be replaced // with '_' // $FILEPATH$ - expands to the destination path where in the project // the file will reside // $FILEPATH_DIR$ - expands to the destination path where in the project // the file will reside with the last path component trimmed // $USER$ - expands to the name of the user running ProjectManager // $DATE$ - expands to the current date as returned by // [[NSDate date] description] // $HOSTNAME$ - the hostname of the machine ProjectManager is running on [string replaceString: @"$PROJECT_NAME$" withString: projectName]; [string replaceString: @"$FILENAME$" withString: [destinationFile lastPathComponent]]; [string replaceString: @"$FILENAME_EXT$" withString: [[destinationFile lastPathComponent] stringByDeletingPathExtension]]; [string replaceString: @"$FILENAME_CAPS$" withString: [[[destinationFile lastPathComponent] uppercaseString] stringByReplacingString: @"." withString: @"_"]]; [string replaceString: @"$FILENAME_LOWER$" withString: [[[destinationFile lastPathComponent] lowercaseString] stringByReplacingString: @"." withString: @"_"]]; [string replaceString: @"$FILEPATH$" withString: destinationFile]; [string replaceString: @"$FILEPATH_DIR$" withString: [destinationFile stringByDeletingLastPathComponent]]; [string replaceString: @"$USER$" withString: NSUserName()]; [string replaceString: @"$DATE$" withString: [[NSDate date] description]]; [string replaceString: @"$HOSTNAME$" withString: [[NSProcessInfo processInfo] hostName]]; if ([string writeToFile: destinationFile atomically: NO]) { return YES; } else { if (error != NULL) { NSDictionary * userInfo; userInfo = [NSDictionary dictionaryWithObject: [NSString stringWithFormat: _(@"Couldn't import file %@: failed to write to destination %@."), sourceFile, destinationFile] forKey: NSLocalizedDescriptionKey]; *error = [NSError errorWithDomain: ProjectCreatorErrorDomain code: ProjectFileImportError userInfo: userInfo]; } return NO; } } /** * Copies `sourceFile' to `destinationFile' and replaces special * project variables (such as $FILENAME$ or $USER$) in the file * while doing so. If, instead, the file is a directory, the filenames * of it's contents are (recursively) replaced. The argument * `projectName' specifies the name of the project that's importing * the file. * * The arguments `sourceFile' and `destinationFile' can be identical, * in which case only variable substitution occurs without any copying. * * @return YES if the import succeeds, otherwise NO. */ BOOL ImportProjectFile(NSString * sourceFile, NSString * destinationFile, NSString * projectName, NSError ** error) { NSFileManager * fm = [NSFileManager defaultManager]; if ([[[fm fileAttributesAtPath: sourceFile traverseLink: YES] fileType] isEqualToString: NSFileTypeDirectory]) { return ImportProjectDirectoryFile(sourceFile, destinationFile, projectName, error); } // otherwise do some variable expansion on the file before copying it else { return ImportProjectPlainFile(sourceFile, destinationFile, projectName, error); } }