/* SubprojectsManager.m Implementation of the SubprojectsManager class for the ProjectManager application. Copyright (C) 2005, 2006 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 "SubprojectsManager.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "../../NSImageAdditions.h" #import "../../ProjectDocument.h" #import "../../ProjectCreator.h" #import "../../ProjectType.h" #import "../../NSArrayAdditions.h" #import "SubprojectsManagerDelegate.h" static void PrintStructure(NSArray * contents, int level) { NSString * prefix = @""; int i; NSEnumerator * e; NSDictionary * entry; for (i = 0; i < level; i++) { prefix = [prefix stringByAppendingString: @" "]; } e = [contents objectEnumerator]; while ((entry = [e nextObject]) != nil) { NSLog(@"%@%@", prefix, [entry objectForKey: @"Name"]); if ([[entry objectForKey: @"Type"] isEqualToString: @"Category"]) { PrintStructure([entry objectForKey: @"Contents"], level + 1); } } } static void SortContentsArrayByName(NSMutableArray * contentsArray) { NSSortDescriptor * sortDescriptor; sortDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"Name" ascending: YES selector: @selector(caseInsensitiveCompare:)] autorelease]; [contentsArray sortUsingDescriptors: [NSArray arrayWithObject: sortDescriptor]]; } @interface SubprojectsManager (Private) - (BOOL) validateControl: control; - (NSMutableArray *) subprojectNamesInArray: (NSArray *) array; - (NSString *) pathToSubprojectFile: (NSDictionary *) subprojectDescription; - (NSMutableArray *) currentCategoryContentsArray; - (NSMutableArray *) parentCategoryContentsArray; - (NSMutableArray *) internalSubprojectNames; - (BOOL) wipeSubprojectsFromCategory: (NSMutableArray *) contentsArray; @end @implementation SubprojectsManager (Private) - (BOOL) validateControl: control { SEL action; // don't allow our controls when we're not visible if ([document currentProjectModule] != self) { return NO; } action = [control action]; if (sel_eq(action, @selector(removeSubprojectAction:)) || sel_eq(action, @selector(openSubprojectAction:))) { int row = [outline selectedRow]; // we have to make sure a real row is selected (by doing [outline // numberOfRows] > [outline selectedRow]) because of a bug in GNUstep // which incorrectly handles selection in outline views if (row >= 0 && [outline numberOfRows] > row) { return [[[outline itemAtRow: row] objectForKey: @"Type"] isEqualToString: @"Subproject"]; } else { return NO; } } else if (sel_eq(action, @selector(removeSubprojectCategoryAction:))) { int row = [outline selectedRow]; if (row >= 0 && [outline numberOfRows] > row) { return [[[outline itemAtRow: row] objectForKey: @"Type"] isEqualToString: @"Category"]; } else { return NO; } } return YES; } /** * Recursively lists all project names under the specified * category contents array. * * @param array An array of subproject and subcategory descriptions, * as contained in the `subprojects' ivar. * * @return An array of subproject names under the given array. */ - (NSMutableArray *) subprojectNamesInArray: (NSArray *) array { NSMutableArray * names = [NSMutableArray array]; NSEnumerator * e = [array objectEnumerator]; NSDictionary * entry; while ((entry = [e nextObject]) != nil) { if ([[entry objectForKey: @"Type"] isEqualToString: @"Subproject"]) { [names addObject: [entry objectForKey: @"Name"]]; } else { [names addObjectsFromArray: [self subprojectNamesInArray: [entry objectForKey: @"Contents"]]]; } } return names; } /** * Returns a path to the subproject file of the subproject described * by the argument dictionary. * * @param subprojectDescription A dictionary describing the subproject, * formatted like the contents of the `subprojects' ivar. * * @return A path to the subproject's project file. */ - (NSString *) pathToSubprojectFile: (NSDictionary *) subprojectDescription { return [[[delegate pathToSubprojectsDirectory] stringByAppendingPathComponent: [subprojectDescription objectForKey: @"Name"]] stringByAppendingPathComponent: [subprojectDescription objectForKey: @"ProjectFile"]]; } /** * Returns the category contents array of the category which contains * the currently selected item, or the item's contents array if the * item selected is a category itself. */ - (NSMutableArray *) currentCategoryContentsArray { int row = [outline selectedRow]; if (row >= 0 && [outline numberOfRows] > row) { NSDictionary * selectedEntry = [outline itemAtRow: row]; // if the selected entry is a category itself, use that if ([[selectedEntry objectForKey: @"Type"] isEqualToString: @"Category"]) { return [selectedEntry objectForKey: @"Contents"]; } // otherwise look for the superentry for (row -= 1; row >= 0; row--) { NSDictionary * entry = [outline itemAtRow: row]; if ([[entry objectForKey: @"Contents"] containsObject: selectedEntry]) { return [entry objectForKey: @"Contents"]; } } } return subprojects; } /** * Returns the category contents array of the category which contains * the currently selected item, but never the current item, even if it * is a category. */ - (NSMutableArray *) parentCategoryContentsArray { int row = [outline selectedRow]; if (row >= 0 && [outline numberOfRows] > row) { NSDictionary * selectedEntry = [outline itemAtRow: row]; for (row -= 1; row >= 0; row--) { NSDictionary * entry = [outline itemAtRow: row]; if ([[entry objectForKey: @"Contents"] containsObject: selectedEntry]) { return [entry objectForKey: @"Contents"]; } } } return subprojects; } /** * Returns a mutable array of subproject names. This method is * here for internal purposes, when we need a mutable version of * the array, instead of an immutable one. */ - (NSMutableArray *) internalSubprojectNames { return [self subprojectNamesInArray: subprojects]; } /** * Deletes the subprojects contained in a category's contents array * (and subcategories recursively as well) from disk. * * @param contentsArray The contents array of the top-level category * which to wipe of subprojects. * * @return YES if the operation succeeds, NO if it doesn't. */ - (BOOL) wipeSubprojectsFromCategory: (NSMutableArray *) contentsArray { NSFileManager * fm = [NSFileManager defaultManager]; int i, n; for (i = 0, n = [contentsArray count]; i < n; i++) { NSDictionary * entry = [contentsArray objectAtIndex: 0]; NSMutableArray * subcontentsArray = [entry objectForKey: @"Contents"]; if (subcontentsArray != nil) { if (![self wipeSubprojectsFromCategory: subcontentsArray]) { return NO; } } else { NSString * subprojectDirectoryPath = [[self pathToSubprojectFile: entry] stringByDeletingLastPathComponent]; if (![fm removeFileAtPath: subprojectDirectoryPath handler: self]) { NSRunAlertPanel(_(@"Error deleting subproject"), _(@"Couldn't delete path %@: %@"), nil, nil, nil, [fileOpErrorDict objectForKey: @"Path"], [fileOpErrorDict objectForKey: @"Error"]); return NO; } } [contentsArray removeObjectAtIndex: 0]; [document updateChangeCount: NSChangeDone]; } return YES; } @end /** * This class is a manager of subprojects for a project. * * It simply identifies subprojects by name and path where they * live and handles adding, removing and opening them. It also * provides the user with the possibility to organize subprojects * into "subproject categories". * * It's delegate must conform to the SubprojectsManagerDelegate * protocol, in order to tell it where to put the subprojects * it manages. */ @implementation SubprojectsManager + (NSString *) moduleName { return @"SubprojectsManager"; } + (NSString *) humanReadableModuleName { return _(@"Subprojects"); } - (NSArray *) moduleMenuItems { return [NSArray arrayWithObjects: PMMakeMenuItem (_(@"Open Subproject"), @selector(openSubprojectAction:), nil, self), PMMakeMenuItem (_(@"New Subproject..."), @selector(addSubprojectAction:), nil, self), PMMakeMenuItem (_(@"Add Subproject..."), @selector(addSubprojectAction:), nil, self), PMMakeMenuItem (_(@"Remove Subproject..."), @selector(removeSubprojectAction:), nil, self), PMMakeMenuItem (_(@"New Subproject Category..."), @selector(newSubprojectCategoryAction:), nil, self), PMMakeMenuItem (_(@"Remove Subproject Category..."), @selector(removeSubprojectCategoryAction:), nil, self), nil]; } - (NSArray *) toolbarItemIdentifiers { return [NSArray arrayWithObjects: @"SubprojectsManagerNewSubprojectItemIdentifier", @"SubprojectsManagerAddSubprojectItemIdentifier", @"SubprojectsManagerRemoveSubprojectItemIdentifier", @"SubprojectsManagerNewCategoryItemIdentifier", @"SubprojectsManagerRemoveCategoryItemIdentifier", nil]; } - (NSToolbarItem *) toolbarItemForItemIdentifier: (NSString *) itemIdentifier { NSToolbarItem * item = [[[NSToolbarItem alloc] initWithItemIdentifier: itemIdentifier] autorelease]; [item setTarget: self]; if ([itemIdentifier isEqualToString: @"SubprojectsManagerNewSubprojectItemIdentifier"]) { [item setLabel: _(@"New")]; [item setImage: [NSImage imageNamed: @"NewSubproject" owner: self]]; [item setAction: @selector(newSubprojectAction:)]; [item setToolTip: _(@"Creates a new subproject")]; } else if ([itemIdentifier isEqualToString: @"SubprojectsManagerAddSubprojectItemIdentifier"]) { [item setLabel: _(@"Add")]; [item setImage: [NSImage imageNamed: @"AddSubproject" owner: self]]; [item setAction: @selector(addSubprojectAction:)]; [item setToolTip: _(@"Adds a subproject to the project")]; } else if ([itemIdentifier isEqualToString: @"SubprojectsManagerRemoveSubprojectItemIdentifier"]) { [item setLabel: _(@"Remove")]; [item setImage: [NSImage imageNamed: @"RemoveSubproject" owner: self]]; [item setAction: @selector(removeSubprojectAction:)]; [item setToolTip: _(@"Removes a subproject from the project")]; } else if ([itemIdentifier isEqualToString: @"SubprojectsManagerNewCategoryItemIdentifier"]) { [item setLabel: _(@"New Category")]; [item setImage: [NSImage imageNamed: @"NewSubprojectCategory" owner: self]]; [item setAction: @selector(newSubprojectCategoryAction:)]; [item setToolTip: _(@"Adds a subprojects category")]; } else if ([itemIdentifier isEqualToString: @"SubprojectsManagerRemoveCategoryItemIdentifier"]) { [item setLabel: _(@"Remove Category")]; [item setImage: [NSImage imageNamed: @"RemoveSubprojectCategory" owner: self]]; [item setAction: @selector(removeSubprojectCategoryAction:)]; [item setToolTip: _(@"Removes a subprojects category")]; } return item; } - (void) dealloc { [[NSNotificationCenter defaultCenter] removeObserver: self]; TEST_RELEASE(view); TEST_RELEASE(subprojects); TEST_RELEASE(subprojectNames); TEST_RELEASE(fileOpErrorDict); [super dealloc]; } - initWithDocument: (ProjectDocument *) aDocument infoDictionary: (NSDictionary *) infoDict { if ((self = [self init]) != nil) { document = aDocument; // the values in this dictionary aren't full paths to the subprojects // yet - we'll need to change these in -setDelegate: where we can // ask the delegate about the subproject directory path ASSIGN(subprojects, [[infoDict objectForKey: @"Subprojects"] makeDeeplyMutableEquivalent]); if (subprojects == nil) { subprojects = [NSMutableArray new]; } // regenerate the subproject names ASSIGN(subprojectNames, [self internalSubprojectNames]); } return self; } - (ProjectDocument *) document { return document; } - (NSView *) view { if (view == nil) { [NSBundle loadNibNamed: @"SubprojectsManager" owner: self]; } return view; } - (NSDictionary *) infoDictionary { return [NSDictionary dictionaryWithObject: subprojects forKey: @"Subprojects"]; } - (BOOL) regenerateDerivedFiles { return YES; } - (BOOL) validateMenuItem: (id ) item { return [self validateControl: item]; } - (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem { return [self validateControl: toolbarItem]; } - (void) awakeFromNib { view = [[bogusWindow contentView] retain]; DESTROY(bogusWindow); [outline setDoubleAction: @selector(openSubprojectAction:)]; } /** * Sets the delegate of the receiver. This method may be invoked * only once! * * @param aDelegate An object which must conform to the * SubprojectsManagerDelegate protocol. */ - (void) finishInit { delegate = (id ) [document projectType]; } /** * Returns absolute paths to all subprojects. */ - (NSArray *) subprojectNames { return [self internalSubprojectNames]; } - (BOOL) addSubproject: (NSString *) aProject toCategory: (NSMutableArray *) contentsArray { NSString * projectFilePath; NSString * sourceDirectory = [aProject stringByDeletingLastPathComponent]; NSString * destinationDirectory; NSString * subprojectName = [[aProject lastPathComponent] stringByDeletingPathExtension]; ProjectDocument * doc; if ([subprojectNames containsObject: subprojectName]) { NSRunAlertPanel(_(@"Subproject already exists"), _(@"The already is a subproject named %@."), nil, nil, nil, subprojectName); return NO; } destinationDirectory = [[delegate pathToSubprojectsDirectory] stringByAppendingPathComponent: [sourceDirectory lastPathComponent]]; // do we have to copy the subproject into the project? if (![destinationDirectory isEqualToString: sourceDirectory]) { NSFileManager * fm = [NSFileManager defaultManager]; NSError * error; // make sure we won't overwrite something if ([fm fileExistsAtPath: destinationDirectory]) { NSRunAlertPanel(_(@"Failed to add subproject"), _(@"A file at %@ is in the way - remove it first."), nil, nil, nil, destinationDirectory); return NO; } // create the intermediate directories if (!CreateDirectoryAndIntermediateDirectories([destinationDirectory stringByDeletingLastPathComponent], &error)) { NSRunAlertPanel(_(@"Failed to add subproject"), _(@"Couldn't copy %@ to the project's subproject directory.\n%@."), nil, nil, nil, sourceDirectory, [[error userInfo] objectForKey: NSLocalizedDescriptionKey]); return NO; } // and copy the thing in if (![fm copyPath: sourceDirectory toPath: destinationDirectory handler: self]) { NSRunAlertPanel(_(@"Failed to add subproject"), _(@"Couldn't copy %@ to the project's subproject directory\n" @"in %@. Error while processing path %@: %@"), nil, nil, nil, [fileOpErrorDict objectForKey: @"FromPath"], [fileOpErrorDict objectForKey: @"ToPath"], [fileOpErrorDict objectForKey: @"Path"], [fileOpErrorDict objectForKey: @"Error"]); return NO; } } // open the subproject's document invisibly, save it and close it again, // in order to make the project regenerate it's derived files projectFilePath = [destinationDirectory stringByAppendingPathComponent: [aProject lastPathComponent]]; doc = [[NSDocumentController sharedDocumentController] makeDocumentWithContentsOfFile: projectFilePath ofType: @"pmproj"]; if (doc == nil) { NSRunAlertPanel(_(@"Failed to add subproject"), _(@"Couldn't open the subproject - perhaps it is corrupt?"), nil, nil, nil); return NO; } [doc saveDocument: nil]; [doc close]; // add the entry [contentsArray addObject: [NSMutableDictionary dictionaryWithObjectsAndKeys: @"Subproject", @"Type", subprojectName, @"Name", [aProject lastPathComponent], @"ProjectFile", nil]]; // and sort the array properly SortContentsArrayByName(contentsArray); // these don't need to be sorted [subprojectNames addObject: subprojectName]; [document updateChangeCount: NSChangeDone]; PrintStructure(subprojects, 0); NSLog(@""); [outline reloadData]; return YES; } - (BOOL) removeSubproject: (NSString *) subprojectName fromCategory: (NSMutableArray *) contentsArray delete: (BOOL) deleteFlag { NSEnumerator * e; NSDictionary * entry; NSString * subprojectDirectory, * subprojectFileName; // first locate the subproject's entry e = [contentsArray objectEnumerator]; while ((entry = [e nextObject]) != nil) { if ([[entry objectForKey: @"Name"] isEqualToString: subprojectName] && [[entry objectForKey: @"Type"] isEqualToString: @"Subproject"]) { break; } } NSAssert(entry != nil, @"Couldn't find associated subproject entry."); subprojectFileName = [self pathToSubprojectFile: entry]; subprojectDirectory = [subprojectFileName stringByDeletingLastPathComponent]; if (deleteFlag) { NSFileManager * fm = [NSFileManager defaultManager]; ProjectDocument * doc; // close an open document doc = [[NSDocumentController sharedDocumentController] documentForFileName: subprojectFileName]; if (doc != nil) { [doc close]; } if (![fm removeFileAtPath: subprojectDirectory handler: self]) { NSRunAlertPanel(_(@"Cannot remove subproject"), _(@"Couldn't delete the subproject's files from disk\n" @"at %@. Error while processing path: %@: %@"), nil, nil, nil, [fileOpErrorDict objectForKey: @"FromPath"], [fileOpErrorDict objectForKey: @"Path"], [fileOpErrorDict objectForKey: @"Error"]); return NO; } } [subprojectNames removeObject: subprojectName]; [contentsArray removeObject: entry]; [document updateChangeCount: NSChangeDone]; [outline reloadItem: nil reloadChildren: YES]; return YES; } - (void) newSubprojectAction: sender { NSDictionary * projectSetup; projectSetup = [[ProjectCreator shared] getNewProjectSetupWithLocation: NO]; if (projectSetup != nil) { NSString * projectName = [projectSetup objectForKey: @"ProjectName"]; NSString * template = [projectSetup objectForKey: @"ProjectTemplate"]; NSString * projectPath = [[delegate pathToSubprojectsDirectory] stringByAppendingPathComponent: projectName]; NSError * error; if (![ProjectCreator createNewProjectAtPath: projectPath projectName: projectName fromTemplate: template error: &error]) { NSRunAlertPanel(_(@"Error creating subproject"), _(@"Unable to create subproject at %@: %@"), nil, nil, nil, projectPath, [[error userInfo] objectForKey: NSLocalizedDescriptionKey]); } else { [self addSubproject: [projectPath stringByAppendingPathComponent: [projectName stringByAppendingPathExtension: @"pmproj"]] toCategory: [self currentCategoryContentsArray]]; } } } - (void) addSubprojectAction: sender { NSOpenPanel * op = [NSOpenPanel openPanel]; [op setCanChooseDirectories: NO]; [op setCanChooseFiles: YES]; [op setAllowsMultipleSelection: NO]; if ([op runModalForTypes: [NSArray arrayWithObject: @"pmproj"]] == NSOKButton) { [self addSubproject: [op filename] toCategory: [self currentCategoryContentsArray]]; } } - (void) removeSubprojectAction: sender { int row = [outline selectedRow]; if (row >= 0 && [outline numberOfRows] > row) { NSString * subprojectName = [[outline itemAtRow: row] objectForKey: @"Name"]; BOOL delete = NO; switch (NSRunAlertPanel(_(@"Really remove subproject?"), _(@"Are you really sure you want to remove %@ from the subprojects?"), _(@"Yes, but keep files on disk"), _(@"Yes and DELETE it from disk"), _(@"Cancel"), subprojectName)) { case NSAlertAlternateReturn: delete = YES; break; case NSAlertOtherReturn: return; } [self removeSubproject: subprojectName fromCategory: [self currentCategoryContentsArray] delete: delete]; } } - (void) openSubprojectAction: sender { int row = [outline selectedRow]; if (row >= 0 && [outline numberOfRows] > row) { NSDictionary * selectedSubproject = [outline itemAtRow: row]; NSString * subprojectName = [selectedSubproject objectForKey: @"Name"]; NSString * path = [self pathToSubprojectFile: selectedSubproject]; ProjectDocument * doc; NSDocumentController * dc = [NSDocumentController sharedDocumentController]; doc = [dc openDocumentWithContentsOfFile: path display: YES]; if (doc != nil) { [doc showWindows]; } else { NSRunAlertPanel(_(@"Failed to open subproject"), _(@"Couldn't open subproject %@"), nil, nil, nil, subprojectName); } } } - (void) newSubprojectCategoryAction: sender { NSMutableArray * parentContentsArray = [self currentCategoryContentsArray]; NSArray * names = [parentContentsArray valueForKey: @"Name"]; NSString * newName = _(@"New Category"); int i; // try to locate a unique name for (i = 1; [names containsObject: newName]; i++) { newName = [NSString stringWithFormat: _(@"New Category %i"), i]; } [parentContentsArray addObject: [NSMutableDictionary dictionaryWithObjectsAndKeys: @"Category", @"Type", newName, @"Name", [NSMutableArray array], @"Contents", nil]]; SortContentsArrayByName(parentContentsArray); PrintStructure(subprojects, 0); NSLog(@""); [outline reloadData]; [document updateChangeCount: NSChangeDone]; } - (void) removeSubprojectCategoryAction: sender { NSDictionary * selectedCategory = [outline itemAtRow: [outline selectedRow]]; NSMutableArray * contentsArray = [selectedCategory objectForKey: @"Contents"]; NSMutableArray * parentContentsArray = [self parentCategoryContentsArray]; if ([contentsArray count] > 0) { switch (NSRunAlertPanel(_(@"Really remove category?"), _(@"Are you sure that you want to remove category %@?\n" @"Also, do you want me to keep any of it's subprojects\n" @"on disk, or DELETE them?"), _(@"Remove, but keep subprojects on disk"), _(@"Remove, and DELETE subprojects from disk"), _(@"Cancel"), [selectedCategory objectForKey: @"Name"])) { case NSAlertDefaultReturn: break; case NSAlertAlternateReturn: if (![self wipeSubprojectsFromCategory: contentsArray]) { [outline reloadData]; return; } break; case NSAlertOtherReturn: return; } } else { if (NSRunAlertPanel(_(@"Really remove category?"), _(@"Are you sure that you want to remove category %@?"), _(@"Remove"), _(@"Cancel"), nil, [selectedCategory objectForKey: @"Name"]) == NSAlertAlternateReturn) { return; } } [parentContentsArray removeObject: selectedCategory]; PrintStructure(subprojects, 0); NSLog(@""); [outline reloadData]; [document updateChangeCount: NSChangeDone]; } - (id)outlineView: (NSOutlineView *)outlineView child: (int)index ofItem: (id)item { if (item != nil) { return [[item objectForKey: @"Contents"] objectAtIndex: index]; } else { return [subprojects objectAtIndex: index]; } } - (BOOL)outlineView: (NSOutlineView *)outlineView isItemExpandable: (id)item { return [[item objectForKey: @"Type"] isEqualToString: @"Category"]; } - (int)outlineView: (NSOutlineView *)outlineView numberOfChildrenOfItem: (id)item { if (item != nil) { return [[item objectForKey: @"Contents"] count]; } else { return [subprojects count]; } } - (id)outlineView: (NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { if ([[tableColumn identifier] isEqualToString: @"Name"]) { return [item objectForKey: @"Name"]; } else { NSString * projectFile = [item objectForKey: @"ProjectFile"]; if (projectFile != nil) { projectFile = [self pathToSubprojectFile: item]; } return projectFile; } } - (void)outlineView: (NSOutlineView *)outlineView setObjectValue: (id)object forTableColumn: (NSTableColumn *)tableColumn byItem: (id)item { [item setObject: object forKey: @"Name"]; SortContentsArrayByName([self parentCategoryContentsArray]); PrintStructure(subprojects, 0); NSLog(@""); [outline reloadData]; } - (BOOL) fileManager: (NSFileManager*)fileManager shouldProceedAfterError: (NSDictionary*)errorDictionary { ASSIGN(fileOpErrorDict, errorDictionary); return NO; } - (void) fileManager: (NSFileManager*)fileManager willProcessPath: (NSString *) aPath {} @end