1/*
2   GNUstep ProjectCenter - http://www.gnustep.org/experience/ProjectCenter.html
3
4   Copyright (C) 2000-2011 Free Software Foundation
5
6   Authors: Philippe C.D. Robert
7            Serg Stoyan
8
9   This file is part of GNUstep.
10
11   This application is free software; you can redistribute it and/or
12   modify it under the terms of the GNU General Public
13   License as published by the Free Software Foundation; either
14   version 2 of the License, or (at your option) any later version.
15
16   This application is distributed in the hope that it will be useful,
17   but WITHOUT ANY WARRANTY; without even the implied warranty of
18   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
19   Library General Public License for more details.
20
21   You should have received a copy of the GNU General Public
22   License along with this library; if not, write to the Free
23   Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA.
24*/
25
26#import <ProjectCenter/PCDefines.h>
27#import <ProjectCenter/PCFileManager.h>
28#import <ProjectCenter/PCFileCreator.h>
29#import <ProjectCenter/PCProjectManager.h>
30#import <ProjectCenter/PCProject.h>
31#import <ProjectCenter/PCProjectBrowser.h>
32#import <ProjectCenter/PCAddFilesPanel.h>
33
34#import <Protocols/Preferences.h>
35#import <ProjectCenter/PCLogController.h>
36
37@implementation PCFileManager
38
39// ===========================================================================
40// ==== Class methods
41// ===========================================================================
42
43static PCFileManager *_mgr = nil;
44
45+ (PCFileManager *)defaultManager
46{
47  if (_mgr == nil)
48    {
49      _mgr = [[self alloc] init];
50    }
51
52  return _mgr;
53}
54
55// ===========================================================================
56// ==== Init and free
57// ===========================================================================
58
59- (id)initWithProjectManager:(PCProjectManager *)aProjectManager
60{
61  if ((self = [super init]))
62    {
63      projectManager = aProjectManager;
64    }
65  return self;
66}
67
68- (void)dealloc
69{
70#ifdef DEVELOPMENT
71  NSLog (@"PCFileManager: dealloc");
72#endif
73
74  if (addFilesPanel)
75    {
76      RELEASE(addFilesPanel);
77    }
78
79  [super dealloc];
80}
81
82// ===========================================================================
83// ==== NSFileManager delegate methods
84// ===========================================================================
85- (BOOL)     fileManager:(NSFileManager *)manager
86 shouldProceedAfterError:(NSDictionary *)errorDict
87{
88  NSLog(@"FM error is: %@", [errorDict objectForKey:@"Error"]);
89
90  return YES;
91}
92
93// ===========================================================================
94// ==== File handling
95// ===========================================================================
96- (BOOL)createDirectoriesIfNeededAtPath:(NSString *)path
97{
98  NSString       *_path = [NSString stringWithString:path];
99  NSString       *_oldPath = nil;
100  NSMutableArray *pathArray = [NSMutableArray array];
101  NSFileManager  *fm = [NSFileManager defaultManager];
102  BOOL           isDir;
103  int            i;
104
105  /* We stop when we find a file, or when we can't remove any path
106   * component any more.  Else, you may end up in an infinite loop if
107   * _path = @"".
108   */
109  isDir = NO;
110  while (_path != nil
111	 &&  ![_path isEqualToString: _oldPath]
112	 &&  ![fm fileExistsAtPath:_path isDirectory:&isDir])
113    {
114      [pathArray addObject:[_path lastPathComponent]];
115      _oldPath = _path;
116      _path = [_path stringByDeletingLastPathComponent];
117    }
118
119  if (!isDir)
120    {
121      return NO;
122    }
123
124  if ([_path length] != [path length])
125    {
126      for (i = [pathArray count]-1; i >= 0; i--)
127	{
128	  _path =
129	    [_path stringByAppendingPathComponent:[pathArray objectAtIndex:i]];
130	  if ([fm createDirectoryAtPath:_path attributes:nil] == NO)
131	    {
132	      return NO;
133	    }
134	}
135    }
136
137  return YES;
138}
139
140- (BOOL)copyFile:(NSString *)file toFile:(NSString *)toFile
141{
142  NSFileManager *fm = [NSFileManager defaultManager];
143  NSString      *directoryPath = nil;
144
145  if (!file)
146    {
147      return NO;
148    }
149
150  if (![fm fileExistsAtPath:toFile])
151    {
152      directoryPath = [toFile stringByDeletingLastPathComponent];
153      if ([self createDirectoriesIfNeededAtPath:directoryPath] == NO)
154	{
155	  NSRunAlertPanel(@"Copy File",
156			  @"Couldn't create directories at path %@",
157			  @"Ok",nil,nil, directoryPath);
158	  return NO;
159	}
160
161      if ([fm copyPath:file toPath:toFile handler:self] == NO)
162	{
163	  NSRunAlertPanel(@"Copy File",
164			  @"Couldn't copy file %@ to %@",
165			  @"Ok",nil,nil, file, toFile);
166	  return NO;
167	}
168    }
169
170  return YES;
171}
172
173- (BOOL)copyFile:(NSString *)file intoDirectory:(NSString *)directory
174{
175  NSString *path = nil;
176
177  if (!file)
178    {
179      return NO;
180    }
181
182  path = [directory stringByAppendingPathComponent:[file lastPathComponent]];
183
184  if (![self copyFile:file toFile:path])
185    { // No need to open aler panel here
186      return NO;
187    }
188
189  return YES;
190}
191
192- (BOOL)copyFile:(NSString *)file
193   fromDirectory:(NSString *)fromDir
194   intoDirectory:(NSString *)toDir
195{
196  NSString *path = nil;
197
198  if (!file || !fromDir || !toDir)
199    {
200      return NO;
201    }
202
203  path = [fromDir stringByAppendingPathComponent:[file lastPathComponent]];
204
205  if (![self copyFile:path intoDirectory:toDir])
206    {
207      return NO;
208    }
209
210  return YES;
211}
212
213- (BOOL)copyFiles:(NSArray *)files intoDirectory:(NSString *)directory
214{
215  NSEnumerator *enumerator = nil;
216  NSString     *file = nil;
217
218  if (!files)
219    {
220      return NO;
221    }
222
223  enumerator = [files objectEnumerator];
224  while ((file = [enumerator nextObject]))
225    {
226      if ([self copyFile:file intoDirectory:directory] == NO)
227	{
228	  return NO;
229	}
230    }
231
232  return YES;
233}
234
235- (BOOL)removeDirectoriesIfEmptyAtPath:(NSString *)path
236{
237  NSFileManager *fm = [NSFileManager defaultManager];
238
239  while ([[fm directoryContentsAtPath:path] count] == 0)
240    {
241      if ([fm removeFileAtPath:path handler:nil] == NO)
242	{
243	  NSRunAlertPanel(@"Remove Directory",
244			  @"Couldn't remove empty directory at path %@",
245			  @"Ok",nil,nil, path);
246	  return NO;
247	}
248      path = [path stringByDeletingLastPathComponent];
249    }
250
251  return YES;
252}
253
254- (BOOL)removeFile:(NSString *)file
255     fromDirectory:(NSString *)directory
256 removeDirsIfEmpty:(BOOL)removeDirs
257{
258  NSString      *path = nil;
259  NSFileManager *fm = [NSFileManager defaultManager];
260
261  if (!file)
262    {
263      return NO;
264    }
265
266  path = [directory stringByAppendingPathComponent:file];
267  if (![fm removeFileAtPath:path handler:nil])
268    {
269      NSRunAlertPanel(@"Remove File",
270		      @"Couldn't remove file at path %@",
271		      @"Ok",nil,nil, path);
272      return NO;
273    }
274
275  if (removeDirs)
276    {
277      [self removeDirectoriesIfEmptyAtPath:directory];
278    }
279
280  return YES;
281}
282
283- (BOOL)removeFileAtPath:(NSString *)file removeDirsIfEmpty:(BOOL)removeDirs
284{
285  return [self removeFile:[file lastPathComponent]
286	    fromDirectory:[file stringByDeletingLastPathComponent]
287	removeDirsIfEmpty:removeDirs];
288}
289
290- (BOOL)removeFiles:(NSArray *)files
291      fromDirectory:(NSString *)directory
292  removeDirsIfEmpty:(BOOL)removeDirs
293{
294  NSEnumerator *filesEnum = nil;
295  NSString     *file = nil;
296
297  if (!files)
298    {
299      return NO;
300    }
301
302  filesEnum = [files objectEnumerator];
303  while ((file = [filesEnum nextObject]))
304    {
305      if ([self removeFile:file
306	     fromDirectory:directory
307	 removeDirsIfEmpty:removeDirs] == NO)
308	{
309	  return NO;
310	}
311    }
312
313  return YES;
314}
315
316- (BOOL)moveFile:(NSString *)file intoDirectory:(NSString *)directory
317{
318  if ([self copyFile:file intoDirectory:directory] == YES)
319    {
320      [self removeFileAtPath:file removeDirsIfEmpty:YES];
321    }
322  else
323    {
324      NSRunAlertPanel(@"Move File",
325		      @"Couldn't move file %@ to %@",
326		      @"Ok",nil,nil, file, directory);
327      return NO;
328    }
329
330  return YES;
331}
332
333// ===========================================================================
334// ==== Find Executable
335// Tries to find the first matching executable tool fromt he given, nil-terminated
336// list. Returns the full path for it.
337// ===========================================================================
338- (NSString*) findExecutableToolFrom: (NSArray*)candidates
339{
340  NSFileManager	*manager;
341  NSEnumerator	*pathEnumerator;
342  NSString	*directory;
343
344  manager = [NSFileManager defaultManager];
345  pathEnumerator = [NSSearchPathForDirectoriesInDomains(NSDeveloperDirectory, NSAllDomainsMask, YES) objectEnumerator];
346
347  while (nil != (directory = [pathEnumerator nextObject]))
348    {
349      NSEnumerator *candidateEnumerator = [candidates objectEnumerator];
350      NSString     *candidate;
351
352      while (nil != (candidate = [candidateEnumerator nextObject]))
353        {
354          NSString *path = [directory stringByAppendingPathComponent: candidate];
355
356          NSLog(@"final candidate path is: %@", path);
357
358          if ([manager isExecutableFileAtPath: path])
359	    {
360	      return path;
361            }
362        }
363    }
364  return nil;
365}
366
367
368@end
369
370@implementation PCFileManager (UInterface)
371
372// ===========================================================================
373// ==== Panels
374// ===========================================================================
375
376- (id)_panelForOperation:(int)op
377		   title:(NSString *)title
378		 accView:(NSView *)accessoryView
379{
380  id <PCPreferences> prefs = [projectManager prefController];
381  NSString           *lastOpenDir;
382  id                 panel;
383
384  operation = op;
385
386  switch (op)
387    {
388    case PCOpenFileOperation:
389      panel = [NSOpenPanel openPanel];
390      [panel setCanChooseFiles:YES];
391      [panel setCanChooseDirectories:NO];
392      lastOpenDir = [prefs stringForKey:@"FileOpenLastDirectory"];
393      break;
394    case PCSaveFileOperation:
395      panel = [NSSavePanel savePanel];
396      lastOpenDir = [prefs stringForKey:@"FileSaveLastDirectory"];
397      break;
398    case PCOpenProjectOperation:
399      panel = [NSOpenPanel openPanel];
400      [panel setAllowsMultipleSelection:NO];
401      [panel setCanChooseFiles:YES];
402      [panel setCanChooseDirectories:YES];
403      lastOpenDir = [prefs stringForKey:@"ProjectOpenLastDirectory"];
404      break;
405    case PCOpenDirectoryOperation:
406      panel = [NSOpenPanel openPanel];
407      [panel setCanChooseFiles:NO];
408      [panel setCanChooseDirectories:YES];
409      lastOpenDir = [prefs stringForKey:@"FileOpenLastDirectory"];
410      break;
411    case PCAddFileOperation:
412      if (addFilesPanel == nil)
413	{
414	  addFilesPanel = [PCAddFilesPanel addFilesPanel];
415	  [addFilesPanel setTreatsFilePackagesAsDirectories: YES];
416	}
417      panel = addFilesPanel;
418      lastOpenDir = [prefs stringForKey:@"FileAddLastDirectory"];
419      break;
420    default:
421      return nil;
422      break;
423    }
424
425  if (!lastOpenDir)
426    {
427      lastOpenDir = NSHomeDirectory();
428    }
429  [panel setDirectory:lastOpenDir];
430  [panel setDelegate:self];
431
432  if (title != nil)
433    {
434      [panel setTitle:title];
435    }
436  if (accessoryView != nil)
437    {
438      [panel setAccessoryView:accessoryView];
439    }
440
441
442  return panel;
443}
444
445- (void)_saveLastDirectoryForPanel:(id)panel
446{
447  id <PCPreferences> prefs = [projectManager prefController];
448  NSString           *key = nil;
449
450  switch (operation)
451    {
452    case PCOpenFileOperation:
453      key = @"FileOpenLastDirectory";
454      break;
455    case PCSaveFileOperation:
456      key = @"FileSaveLastDirectory";
457      break;
458    case PCOpenProjectOperation:
459      key = @"ProjectOpenLastDirectory";
460      break;
461    case PCAddFileOperation:
462      key = @"FileAddLastDirectory";
463      break;
464    default:
465      break;
466    }
467
468  if (key != nil)
469    {
470      [prefs setString:[panel directory] forKey:key notify:NO];
471    }
472}
473
474- (NSMutableArray *)filesOfTypes:(NSArray *)types
475		       operation:(int)op
476			multiple:(BOOL)yn
477			   title:(NSString *)title
478			 accView:(NSView *)accessoryView
479{
480  id             panel;
481  NSMutableArray *fileList = [[NSMutableArray alloc] init];
482  int            result = -10;
483
484  panel = [self _panelForOperation:op title:title accView:accessoryView];
485  if (types != nil)
486    {
487      [panel setAllowedFileTypes:types];
488    }
489
490  if ((op == PCOpenFileOperation) ||
491      (op == PCOpenProjectOperation) ||
492      (op == PCOpenDirectoryOperation))
493    {
494      if ((result = [panel runModalForTypes:types]) == NSOKButton)
495	{
496	  [fileList addObjectsFromArray:[panel filenames]];
497	}
498    }
499  else if (op == PCSaveFileOperation)
500    {
501      if ((result = [panel runModal]) == NSOKButton)
502	{
503	  [fileList addObject:[panel filename]];
504	}
505    }
506  else if (op == PCAddFileOperation)
507    {
508      PCProject *project = [projectManager activeProject];
509      NSString  *selectedCategory = nil;
510
511      [panel setCategories:[project rootCategories]];
512      selectedCategory = [[project projectBrowser] nameOfSelectedCategory];
513      [panel selectCategory:selectedCategory];
514
515      if ((result = [panel runModalForTypes:types]) == NSOKButton)
516	{
517	  [fileList addObjectsFromArray:[panel filenames]];
518	}
519    }
520
521  if (result == NSOKButton)
522    {
523      [self _saveLastDirectoryForPanel:panel];
524      return [fileList autorelease];
525    }
526
527  return nil;
528}
529
530// ============================================================================
531// ==== PCAddFilesPanel delegate
532// ============================================================================
533
534- (void)categoryChangedTo:(NSString *)category
535{
536  PCProject        *project = [projectManager activeProject];
537  NSArray          *fileTypes = nil;
538  PCProjectBrowser *browser = [project projectBrowser];
539  NSString         *path = [browser path];
540
541  [addFilesPanel setTitle:[NSString stringWithFormat:@"Add %@",category]];
542
543  fileTypes = [project
544    fileTypesForCategoryKey:[project keyForCategory:category]];
545  [addFilesPanel setFileTypes:fileTypes];
546
547  // Set project browser path
548  path = [path stringByDeletingLastPathComponent];
549  path = [path stringByAppendingPathComponent:category];
550  [browser setPath:path];
551}
552
553// ============================================================================
554// ==== NSOpenPanel and NSSavePanel delegate
555// ============================================================================
556
557// If file name already in project -- don't show it!
558- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)filename
559{
560  NSFileManager *fileManager = [NSFileManager defaultManager];
561  BOOL          isDir;
562  PCProject     *project = nil;
563  NSArray       *fileTypes = nil;
564  NSString      *category = nil;
565  NSString      *categoryKey = nil;
566
567  [fileManager fileExistsAtPath:filename isDirectory:&isDir];
568
569  if ([[filename pathExtension] isEqualToString:@"gorm"])
570    {
571      isDir = NO;
572    }
573
574  if (sender == addFilesPanel && !isDir)
575    {
576      project = [projectManager activeProject];
577      category = [addFilesPanel selectedCategory];
578      categoryKey = [project keyForCategory:category];
579      fileTypes = [project fileTypesForCategoryKey:categoryKey];
580      // Wrong file extension
581      if (fileTypes
582	  && ![fileTypes containsObject:[filename pathExtension]])
583	{
584	  return NO;
585	}
586      // File is already in project
587      if (![project doesAcceptFile:filename forKey:categoryKey])
588	{
589	  return NO;
590	}
591    }
592
593  return YES;
594}
595
596// Test if we should accept file name selected or entered
597- (BOOL)panel:(id)sender isValidFilename:(NSString *)filename
598{
599  NSFileManager *fm = [NSFileManager defaultManager];
600  BOOL          isDir;
601  NSEnumerator  *e = nil;
602  NSArray       *tempList = nil;
603  NSString      *tempExtension = nil;
604
605  if (operation == PCOpenProjectOperation)
606    {
607      if ([fm fileExistsAtPath:filename isDirectory:&isDir] && isDir)
608	{
609	  e = [[sender allowedFileTypes] objectEnumerator];
610	  while ((tempExtension = [e nextObject]) != nil)
611	    {
612	      tempList = [self filesWithExtension:tempExtension
613				  	   atPath:filename
614				      includeDirs:YES];
615	      if ([tempList count] > 0)
616		{
617		  return YES;
618		}
619	    }
620
621	  return NO;
622	}
623    }
624
625  return YES;
626}
627
628@end
629
630@implementation PCFileManager (Misc)
631
632/**
633 * Returns YES if the file identified by `filename' is a text file,
634 * otherwise returns NO.
635 *
636 * The test is one by reading the first 512 bytes of the file
637 * and checking whether at least 90% of the data are printable
638 * ASCII characters.
639 *
640 * Author Saso Kiselkov
641 */
642- (BOOL)isTextFile:(NSString *)filename
643{
644  NSFileHandle *fh;
645  NSData       *data;
646  NSUInteger i, printable = 0;
647  NSString *content;
648  NSCharacterSet *alpha = [NSCharacterSet alphanumericCharacterSet];
649  NSCharacterSet *spaces = [NSCharacterSet whitespaceAndNewlineCharacterSet];
650  NSCharacterSet *marks = [NSCharacterSet punctuationCharacterSet];
651
652  fh = [NSFileHandle fileHandleForReadingAtPath:filename];
653  if (fh == nil)
654    {
655      return NO;
656    }
657
658  data = [fh readDataOfLength:512];
659  if ([data length] == 0)
660    {
661      return YES;
662    }
663
664  content = [NSString stringWithContentsOfFile: filename];
665  for (i = 0; i < [content length]; i++)
666    {
667      if ([alpha characterIsMember: [content characterAtIndex: i]] ||
668	  [spaces characterIsMember: [content characterAtIndex: i]] ||
669	  [marks characterIsMember: [content characterAtIndex: i]])
670	{
671	  printable++;
672	}
673    }
674
675  return (((double) printable / i) > 0.9);
676}
677
678- (NSArray *)filesWithExtension:(NSString *)extension
679	     		 atPath:(NSString *)dirPath
680     		    includeDirs:(BOOL)incDirs
681{
682  NSFileManager  *fm = [NSFileManager defaultManager];
683  NSMutableArray *filesList = [[NSMutableArray alloc] init];
684  NSEnumerator   *e = nil;
685  NSString       *temp = nil;
686  BOOL           isDir;
687
688  e = [[fm directoryContentsAtPath:dirPath] objectEnumerator];
689  while ((temp = [e nextObject]) != nil)
690    {
691      if ([fm fileExistsAtPath:temp isDirectory:&isDir] && isDir && !incDirs)
692	{
693	  continue;
694	}
695
696      if ([[temp pathExtension] isEqual:extension])
697	{
698	  [filesList addObject:[dirPath stringByAppendingPathComponent:temp]];
699	}
700    }
701
702  return [filesList autorelease];
703}
704
705@end
706