1/*
2   GNUstep ProjectCenter - http://www.gnustep.org/experience/ProjectCenter.html
3
4   Copyright (C) 2000-2004 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/PCProjectManager.h>
29#import <ProjectCenter/PCProject.h>
30#import <ProjectCenter/PCProjectBrowser.h>
31#import <ProjectCenter/PCProjectEditor.h>
32#import <ProjectCenter/PCFileNameField.h>
33
34#import <ProjectCenter/PCLogController.h>
35
36#import "Modules/Preferences/Misc/PCMiscPrefs.h"
37
38NSString *PCBrowserDidSetPathNotification = @"PCBrowserDidSetPathNotification";
39
40@implementation PCProjectBrowser
41
42// ============================================================================
43// ==== Intialization & deallocation
44// ============================================================================
45
46- (id)initWithProject:(PCProject *)aProject
47{
48  if ((self = [super init]))
49    {
50      project = aProject;
51
52      browser = [[NSBrowser alloc] initWithFrame:NSMakeRect(-10,-10,256,128)];
53      [browser setRefusesFirstResponder:YES];
54//      [browser setAutoresizingMask: NSViewWidthSizable | NSViewMinYMargin];
55      [browser setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable];
56      [browser setTitled:NO];
57      [browser setMaxVisibleColumns:4];
58      [browser setSeparatesColumns:NO];
59      [browser setAllowsMultipleSelection:YES];
60      [browser setDelegate:self];
61      [browser setTarget:self];
62      [browser setAction:@selector(click:)];
63      [browser setDoubleAction:@selector(doubleClick:)];
64      [browser setRefusesFirstResponder:YES];
65      [browser loadColumnZero];
66
67      [[NSNotificationCenter defaultCenter]
68	addObserver:self
69	   selector:@selector(projectDictDidChange:)
70	       name:PCProjectDictDidChangeNotification
71	     object:nil];
72    }
73
74  return self;
75}
76
77- (void)dealloc
78{
79#ifdef DEVELOPMENT
80  NSLog (@"PCProjectBrowser: dealloc");
81#endif
82
83  [[NSNotificationCenter defaultCenter] removeObserver:self];
84
85  RELEASE(browser);
86
87  [super dealloc];
88}
89
90// ============================================================================
91// ==== Accessory methods
92// ============================================================================
93
94- (NSView *)view
95{
96  return browser;
97}
98
99// Returns nil if multiple files or category selected
100- (NSString *)nameOfSelectedFile
101{
102  NSString       *name = [[browser path] lastPathComponent];
103  NSString       *category = [self nameOfSelectedCategory];
104  NSMutableArray *pathArray;
105  NSEnumerator   *enumerator;
106  NSString       *pathItem;
107
108//  NSLog(@"---> Selected: %@: category: %@", name, category);
109
110  if ([[browser selectedCells] count] != 1 ||
111      !category ||
112      [name isEqualToString:category])
113    {
114      return nil;
115    }
116
117  pathArray = [[[browser path] pathComponents] mutableCopy];
118  enumerator = [pathArray objectEnumerator];
119  while ((pathItem = [enumerator nextObject]))
120    {
121      if ([pathItem isEqualToString:category])
122	{
123	  name = [enumerator nextObject];
124	  break;
125	}
126    }
127  RELEASE(pathArray);
128
129  return name;
130}
131
132// Returns nil if multiple files selected
133- (NSString *)pathToSelectedFile
134{
135  NSString *name = [self nameOfSelectedFile];
136  NSString *path = [browser path];
137
138  if (!name)
139    {
140      path = nil;
141    }
142
143  return path;
144}
145
146// Returns 'nil' if selected:
147// - root project (browser path is @"/")
148// - multiple categories
149// - name of subproject
150// Should not call any of the nameOf... or pathTo... methods to prevent
151// cyclic recursion.
152- (NSString *)nameOfSelectedCategory
153{
154  NSArray   *pathArray = [[browser path] componentsSeparatedByString:@"/"];
155  NSString  *lastPathElement = [[browser path] lastPathComponent];
156  PCProject *activeProject = [[project projectManager] activeProject];
157  NSArray   *rootCategories = [activeProject rootCategories];
158  NSString  *name = nil;
159  int       i;
160
161  // Name of subproject selected: Change active project to superproject
162  // to check category against superproject's catgory list.
163  // But: path '/Subproject/Foo' and '/Subprojects/Foo/Subprojects' will
164  // return the same category 'Subprojects' and active project will be 'Foo'
165  // in both cases
166//      ![[self nameOfSelectedFile] isEqualToString:lastPathElement])
167/*  if ([lastPathElement isEqualToString:[activeProject projectName]])
168    {
169      activeProject = [activeProject superProject];
170      rootCategories = [activeProject rootCategories];
171    }*/
172
173  // Multiple categories selected
174  if (([rootCategories containsObject:lastPathElement]
175	  && [[browser selectedCells] count] > 1))
176    {
177      return nil;
178    }
179
180  for (i = [pathArray count] - 1; i >= 0; i--)
181    {
182      if ([rootCategories containsObject:[pathArray objectAtIndex:i]])
183	{
184	  name = [pathArray objectAtIndex:i];
185	  break;
186	}
187    }
188
189  // Subproject's name selected
190  if ([name isEqualToString:@"Subprojects"] &&
191      [lastPathElement isEqualToString:[activeProject projectName]])
192    {
193      return nil;
194    }
195
196  return name;
197}
198
199// Returns nil of multiple categories selected
200- (NSString *)pathToSelectedCategory
201{
202  NSString       *path = nil;
203  NSString       *selectedCategory = [self nameOfSelectedCategory];
204  NSMutableArray *bPathArray = nil;
205  int            i;
206
207  if (selectedCategory)
208    {
209      bPathArray = [NSMutableArray arrayWithArray:[[browser path]
210	componentsSeparatedByString:@"/"]];
211      i = [bPathArray count] - 1;
212      while (![[bPathArray objectAtIndex:i] isEqualToString:selectedCategory])
213	{
214	  [bPathArray removeObjectAtIndex:i];
215	  i = [bPathArray count] - 1;
216	}
217      path = [bPathArray componentsJoinedByString:@"/"];
218    }
219
220  return path;
221}
222
223// Returns nil of multiple categories selected
224- (NSString *)pathFromSelectedCategory
225{
226  NSString       *selectedCategory = [self nameOfSelectedCategory];
227  NSMutableArray *bPathArray;
228  NSString       *path = nil;
229
230  if (selectedCategory)
231    {
232      bPathArray =
233	[[[browser path] componentsSeparatedByString:@"/"] mutableCopy];
234      while (![[bPathArray objectAtIndex:1] isEqualToString:selectedCategory])
235	{
236	  [bPathArray removeObjectAtIndex:1];
237	}
238      path = [bPathArray componentsJoinedByString:@"/"];
239      RELEASE(bPathArray);
240    }
241
242  return path;
243}
244
245- (NSString *)nameOfSelectedRootCategory
246{
247  NSString *categoryPath = [self pathToSelectedCategory];
248  NSArray  *pathComponents;
249
250  if ([categoryPath isEqualToString:@"/"] || [categoryPath isEqualToString:@""])
251    {
252      return nil;
253    }
254
255  pathComponents = [categoryPath componentsSeparatedByString:@"/"];
256
257  return [pathComponents objectAtIndex:1];
258}
259
260- (NSArray *)selectedFiles
261{
262  NSArray        *cells = [browser selectedCells];
263  NSMutableArray *files = [[NSMutableArray alloc] initWithCapacity: 1];
264  int            i;
265  int            count = [cells count];
266  PCProject      *activeProject = [[project projectManager] activeProject];
267
268  // Return nil if categories selected
269  if ([cells count] == 0
270      || [[activeProject rootCategories]
271      containsObject:[[cells objectAtIndex:0] stringValue]])
272    {
273      return nil;
274    }
275
276  for (i = 0; i < count; i++)
277    {
278      [files addObject: [[cells objectAtIndex: i] stringValue]];
279    }
280
281  return AUTORELEASE((NSArray *)files);
282}
283
284- (NSString *)path
285{
286  return [browser path];
287}
288
289- (BOOL)setPath:(NSString *)path
290{
291  BOOL res;
292
293  if ([[browser path] isEqualToString: path])
294    {
295      return YES;
296    }
297
298//  PCLogInfo(self, @"[setPath]: %@", path);
299
300  res = [browser setPath:path];
301
302  [[NSNotificationCenter defaultCenter]
303    postNotificationName:PCBrowserDidSetPathNotification
304                  object:self];
305
306  return res;
307}
308
309- (void)reloadLastColumnAndNotify:(BOOL)yn
310{
311  NSInteger column = [browser lastColumn];
312  NSString  *category = [self nameOfSelectedCategory];
313  NSInteger selectedColumn = [browser selectedColumn];
314  NSMatrix  *colMatrix = [browser matrixInColumn:selectedColumn];
315  NSInteger rowCount = 0, colCount = 0, spCount = 0;
316  PCProject *activeProject = [[project projectManager] activeProject];
317  NSString  *selCellTitle = [[browser selectedCell] stringValue];
318
319  if ([category isEqualToString:@"Subprojects"]
320      && ![selCellTitle isEqualToString:@"Subprojects"])
321    { // /Subprojects/Name selected
322      if ([selCellTitle isEqualToString:[activeProject projectName]])
323	{
324	  activeProject = [activeProject superProject];
325	}
326      [colMatrix getNumberOfRows:&rowCount columns:&colCount];
327      spCount = [[[activeProject projectDict]
328	objectForKey:PCSubprojects] count];
329    }
330
331  if ([category isEqualToString:@"Subprojects"] && rowCount != spCount
332      && ![[[browser selectedCell] stringValue] isEqualToString:@"Subprojects"])
333    {
334      column = selectedColumn;
335    }
336
337  [browser reloadColumn:column];
338
339  if (yn)
340    {
341      [[NSNotificationCenter defaultCenter]
342	postNotificationName:PCBrowserDidSetPathNotification
343                      object:self];
344    }
345}
346
347- (void)reloadLastColumnAndSelectFile:(NSString *)file
348{
349  PCProject *p = [[project projectManager] activeProject];
350  NSString  *catKey = [p keyForCategory:[self nameOfSelectedCategory]];
351  NSArray   *array = [[p projectDict] objectForKey:catKey];
352  NSString  *path = [self path];
353  NSString  *tmp;
354
355  // Determine last column with files (removing classes and methods from path)
356  tmp = [[path lastPathComponent] substringWithRange:NSMakeRange(0,1)];
357  while ([tmp isEqualToString:@"@"]     // classes
358	 || [tmp isEqualToString:@"+"]  // factory methods
359	 || [tmp isEqualToString:@"-"]) // instance methods
360    {
361      path = [path stringByDeletingLastPathComponent];
362      tmp = [[path lastPathComponent] substringWithRange:NSMakeRange(0,1)];
363    }
364
365  NSLog(@"PCBrowser set path: %@", path);
366  [self setPath:[path stringByDeletingLastPathComponent]];
367  [self reloadLastColumnAndNotify:NO];
368
369  [browser selectRow:[array indexOfObject:file] inColumn:[browser lastColumn]];
370
371  // Notify
372  [[NSNotificationCenter defaultCenter]
373    postNotificationName:PCBrowserDidSetPathNotification
374                  object:self];
375}
376
377// ============================================================================
378// ==== Actions
379// ============================================================================
380
381- (void)click:(id)sender
382{
383  NSString  *category;
384  PCProject *activeProject;
385  NSString  *browserPath;
386  NSString  *filePath;
387  NSString  *fileName;
388
389  if (sender != browser)
390    {
391      return;
392    }
393
394  category = [self nameOfSelectedCategory];
395  activeProject = [[project projectManager] activeProject];
396  browserPath = [self path];
397  filePath = [self pathToSelectedFile];
398  fileName = [self nameOfSelectedFile];
399
400  NSLog(@"[click] category: %@ forProject: %@ fileName: %@",
401	category, [activeProject projectName], fileName);
402
403//  ![fileName isEqualToString:[activeProject projectName]] &&
404  if (filePath &&
405      [filePath isEqualToString:browserPath] &&
406      category &&
407      ![category isEqualToString:@"Libraries"]
408      )
409    {
410      NSLog(@"[click] category: %@ filePath: %@", category, filePath);
411      [[activeProject projectEditor] openEditorForCategoryPath:browserPath
412					    	      windowed:NO];
413    }
414
415  [[NSNotificationCenter defaultCenter]
416    postNotificationName:PCBrowserDidSetPathNotification
417                  object:self];
418}
419
420- (void)doubleClick:(id)sender
421{
422  NSString    *category = [self nameOfSelectedCategory];
423  id          selectedCell;
424  NSString    *fileName;
425  PCProject   *activeProject;
426  NSString    *key;
427  NSString    *filePath;
428  id <PCPreferences> prefs = [[project projectManager] prefController];
429  NSWorkspace *workspace;
430  NSString    *appName, *type;
431
432  if ((sender != browser) || [category isEqualToString:@"Libraries"])
433    {
434      return;
435    }
436
437  selectedCell = [sender selectedCell];
438  fileName = [[sender selectedCell] stringValue];
439  activeProject = [[project projectManager] activeProject];
440  key = [activeProject keyForCategory:category];
441  filePath = [activeProject pathForFile:fileName forKey:key];
442
443  if ([self nameOfSelectedFile] != nil)
444    {
445      BOOL foundApp = NO;
446      // PCLogInfo(self, @"{doubleClick} filePath: %@", filePath);*/
447
448      workspace = [NSWorkspace sharedWorkspace];
449      foundApp = [workspace getInfoForFile:filePath
450			    application:&appName
451				   type:&type];
452      // NSLog (@"Open file: %@ with app: %@", filePath, appName);
453
454      // If 'Editor' role was set in .GNUstepExtPrefs application
455      // name will be returned according that setting. Otherwise
456      // 'ProjectCenter.app' will be returned accoring to NSTypes
457      // from Info-gnustep.plist file of PC.
458      if(foundApp == NO || [appName isEqualToString:@"ProjectCenter.app"])
459	{
460	  appName = [prefs stringForKey:Editor];
461
462	  if (![appName isEqualToString:@"ProjectCenter"])
463	    {
464	      [workspace openFile:filePath
465		  withApplication:appName];
466	    }
467	  else
468	    {
469	      [[activeProject projectEditor]
470		openEditorForCategoryPath:[self path]
471				 windowed:YES];
472	    }
473	}
474      else
475	{
476	  [workspace openFile:filePath];
477	}
478    }
479  else
480    {
481      if ([[selectedCell title] isEqualToString:@"Subprojects"])
482	{
483	  [[project projectManager] addSubproject];
484	}
485      else
486	{
487	  [[project projectManager] addProjectFiles];
488	}
489    }
490}
491
492// ============================================================================
493// ==== Notifications
494// ============================================================================
495
496- (void)projectDictDidChange:(NSNotification *)aNotif
497{
498  NSDictionary *notifObject = [aNotif object];
499  PCProject    *changedProject = [notifObject objectForKey:@"Project"];
500  NSString     *changedAttribute = [notifObject objectForKey:@"Attribute"];
501
502  if (!browser)
503    {
504      return;
505    }
506
507  if (changedProject != project
508      && changedProject != [project activeSubproject]
509      && [changedProject superProject] != [project activeSubproject])
510    {
511      return;
512    }
513
514//  NSLog(@"PCPB: projectDictDidChange in %@ (%@)",
515//	[changedProject projectName], [project projectName]);
516
517  // If project dictionary changed after files adding/removal,
518  // refresh file list
519  if ([[changedProject rootKeys] containsObject:changedAttribute])
520    {
521      [self reloadLastColumnAndNotify:YES];
522    }
523}
524
525@end
526
527@implementation PCProjectBrowser (ProjectBrowserDelegate)
528
529- (void)     browser:(NSBrowser *)sender
530 createRowsForColumn:(NSInteger)column
531	    inMatrix:(NSMatrix *)matrix
532{
533  NSString   *pathToCol;
534  NSArray    *files;
535  NSUInteger i = 0;
536  NSUInteger count = 0;
537
538  if (sender != browser || !matrix || ![matrix isKindOfClass:[NSMatrix class]])
539    {
540      return;
541    }
542
543  pathToCol = [sender pathToColumn:column];
544  files = [project contentAtCategoryPath:pathToCol];
545  if (files)
546    {
547      count = [files count];
548    }
549
550  for (i = 0; i < count; ++i)
551    {
552      NSMutableString *categoryPath = nil;
553      id              cell;
554
555      categoryPath = [NSMutableString stringWithString:pathToCol];
556
557      [matrix insertRow:i];
558
559      cell = [matrix cellAtRow:i column:0];
560      [cell setStringValue:[files objectAtIndex:i]];
561
562      if (![categoryPath isEqualToString:@"/"])
563	{
564	  [categoryPath appendString:@"/"];
565	}
566      [categoryPath appendString:[files objectAtIndex:i]];
567
568      [cell setLeaf:![project hasChildrenAtCategoryPath:categoryPath]];
569      [cell setRefusesFirstResponder:YES];
570    }
571}
572
573@end
574
575@implementation PCProjectBrowser (FileNameIconDelegate)
576
577// If file was opened in editor:
578// 1. Determine editor
579// 2. Ask editor for icon
580- (NSImage *)_editorIconImageForFile:(NSString *)fileName
581{
582  PCProjectEditor *projectEditor = [project projectEditor];
583  id<CodeEditor>  editor = nil;
584  NSString        *categoryName = [self nameOfSelectedCategory];
585  NSString        *categoryKey = [project keyForCategory:categoryName];
586  NSString        *filePath;
587
588  filePath = [project pathForFile:fileName forKey:categoryKey];
589  editor = [projectEditor editorForFile:filePath];
590  if (editor != nil)
591    {
592      return [editor fileIcon];
593    }
594
595  return nil;
596}
597
598- (NSImage *)fileNameIconImage
599{
600  NSString  *categoryName = nil;
601  NSString  *fileName = nil;
602  NSString  *fileExtension = nil;
603  NSString  *iconName = nil;
604  NSImage   *icon = nil;
605  PCProject *activeProject = [[project projectManager] activeProject];
606
607  fileName = [self nameOfSelectedFile];
608  if (fileName)
609    {
610      if ((icon = [self _editorIconImageForFile:fileName]))
611	{
612	  return icon;
613	}
614      fileExtension = [fileName pathExtension];
615    }
616  else
617    {
618      categoryName = [self nameOfSelectedCategory];
619    }
620
621/*  PCLogError(self,@"{setFileIcon} file %@ category %@",
622	    fileName, categoryName);*/
623
624  if ([[self selectedFiles] count] > 1)
625    {
626      iconName = [[NSString alloc] initWithString:@"MultiFiles"];
627    }
628  // Nothing or subproject name selected
629  else if ((!categoryName && !fileName) ||
630	   [fileName isEqualToString:[activeProject projectName]])
631    {
632      iconName = [[NSString alloc] initWithString:@"FileProject"];
633    }
634  else if ([categoryName isEqualToString: @"Classes"])
635    {
636      iconName = [[NSString alloc] initWithString:@"classSuitcase"];
637    }
638  else if ([categoryName isEqualToString: @"Headers"])
639    {
640      iconName = [[NSString alloc] initWithString:@"headerSuitcase"];
641    }
642  else if ([categoryName isEqualToString: @"Other Sources"])
643    {
644      iconName = [[NSString alloc] initWithString:@"genericSuitcase"];
645    }
646  else if ([categoryName isEqualToString: @"Interfaces"])
647    {
648      iconName = [[NSString alloc] initWithString:@"nibSuitcase"];
649    }
650  else if ([categoryName isEqualToString: @"Images"])
651    {
652      iconName = [[NSString alloc] initWithString:@"iconSuitcase"];
653    }
654  else if ([categoryName isEqualToString: @"Other Resources"])
655    {
656      iconName = [[NSString alloc] initWithString:@"otherSuitcase"];
657    }
658  else if ([categoryName isEqualToString: @"Subprojects"])
659    {
660      iconName = [[NSString alloc] initWithString:@"subprojectSuitcase"];
661    }
662  else if ([categoryName isEqualToString: @"Documentation"])
663    {
664      iconName = [[NSString alloc] initWithString:@"helpSuitcase"];
665    }
666  else if ([categoryName isEqualToString: @"Supporting Files"])
667    {
668      iconName = [[NSString alloc] initWithString:@"genericSuitcase"];
669    }
670  else if ([categoryName isEqualToString: @"Libraries"])
671    {
672      iconName = [[NSString alloc] initWithString:@"librarySuitcase"];
673    }
674  else if ([categoryName isEqualToString: @"Non Project Files"])
675    {
676      iconName = [[NSString alloc] initWithString:@"projectSuitcase"];
677    }
678
679  if (iconName != nil)
680    {
681      icon = IMAGE(iconName);
682      RELEASE(iconName);
683    }
684  else
685    {
686      icon = [[NSWorkspace sharedWorkspace] iconForFile:fileName];
687    }
688
689  return icon;
690}
691
692- (NSString *)fileNameIconTitle
693{
694  NSString *categoryName = [self nameOfSelectedCategory];
695  NSString *fileName = [self nameOfSelectedFile];
696  int      filesCount = [[self selectedFiles] count];
697
698  if (filesCount > 1)
699    {
700      return [NSString stringWithFormat:@"%i files", filesCount];
701    }
702  else if (fileName)
703    {
704      return fileName;
705    }
706  else if (categoryName)
707    {
708      return categoryName;
709    }
710
711  return PCFileNameFieldNoFiles;
712}
713
714- (NSString *)fileNameIconPath
715{
716  NSString *fileName = [self nameOfSelectedFile];
717  NSString *category = [self nameOfSelectedCategory];
718
719  return [project pathForFile:fileName
720		       forKey:[project keyForCategory:category]];
721}
722
723- (BOOL)canPerformDraggingOf:(NSArray *)paths
724{
725  NSString     *category = [self nameOfSelectedCategory];
726  NSString     *categoryKey = [project keyForCategory:category];
727  NSArray      *fileTypes = [project fileTypesForCategoryKey:categoryKey];
728  NSEnumerator *e = [paths objectEnumerator];
729  NSString     *s;
730
731  NSLog(@"PCBrowser: canPerformDraggingOf -> %@", category);
732
733  if (!category || ([self nameOfSelectedFile] != nil))
734    {
735      return NO;
736    }
737
738  if (![project isEditableCategory:category])
739    {
740      return NO;
741    }
742
743  // Check if we can accept files of such types
744  while ((s = [e nextObject]))
745    {
746      if (![fileTypes containsObject:[s pathExtension]])
747	{
748	  return NO;
749	}
750    }
751
752  return YES;
753}
754
755- (BOOL)prepareForDraggingOf:(NSArray *)paths
756{
757  return YES;
758}
759
760- (BOOL)performDraggingOf:(NSArray *)paths
761{
762  NSString     *category = [self nameOfSelectedCategory];
763  NSString     *categoryKey = [project keyForCategory:category];
764  NSEnumerator *pathsEnum = [paths objectEnumerator];
765  NSString     *file = nil;
766
767  while ((file = [[pathsEnum nextObject] lastPathComponent]))
768    {
769      if (![project doesAcceptFile:file forKey:categoryKey])
770	{
771	  return NO;
772	}
773    }
774
775  return [project addAndCopyFiles:paths forKey:categoryKey];
776}
777
778@end
779