/*
    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 <Foundation/NSArray.h>
#import <Foundation/NSData.h>
#import <Foundation/NSBundle.h>
#import <Foundation/NSError.h>
#import <Foundation/NSException.h>
#import <Foundation/NSFileManager.h>
#import <Foundation/NSFileHandle.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSString.h>
#import <Foundation/NSUserDefaults.h>

#import <AppKit/NSBrowserCell.h>
#import <AppKit/NSBrowser.h>
#import <AppKit/NSButton.h>
#import <AppKit/NSDragging.h>
#import <AppKit/NSImage.h>
#import <AppKit/NSMatrix.h>
#import <AppKit/NSMenuItem.h>
#import <AppKit/NSNibLoading.h>
#import <AppKit/NSOpenPanel.h>
#import <AppKit/NSPanel.h>
#import <AppKit/NSPasteboard.h>
#import <AppKit/NSTextField.h>
#import <AppKit/NSToolbarItem.h>
#import <AppKit/NSWorkspace.h>

#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 <FileManagerDelegate>) [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 <NSDraggingInfo>) 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 <NSMenuItem>) menuItem
{
  return [self validateAction: [menuItem action]];
}

- (BOOL) validateToolbarItem: (NSToolbarItem *) toolbarItem
{
  return [self validateAction: [toolbarItem action]];
}

- (BOOL) regenerateDerivedFiles
{
  return YES;
}

@end
