mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-29 23:13:36 +00:00
473 lines
18 KiB
Objective-C
473 lines
18 KiB
Objective-C
/*
|
|
* This file is a part of program xcodecapp-cocoa
|
|
* Copyright (C) 2011 Antoine Mercadal (primalmotion@archipelproject.org)
|
|
*
|
|
* 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 3 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, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
|
|
#import "AppController.h"
|
|
|
|
void fsevents_callback(ConstFSEventStreamRef streamRef, void *userData, size_t numEvents,
|
|
void *eventPaths, const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[])
|
|
{
|
|
AppController *ac = (AppController *)userData;
|
|
size_t i;
|
|
for(i = 0; i < numEvents; i++)
|
|
{
|
|
[ac handleFileModification:[(NSArray *)eventPaths objectAtIndex:i] ignoreDate:NO];
|
|
[ac updateLastEventId:eventIds[i]];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@implementation AppController
|
|
|
|
#pragma mark -
|
|
#pragma mark Initialization
|
|
|
|
- (id)init
|
|
{
|
|
self = [super init];
|
|
|
|
if (self)
|
|
{
|
|
[GrowlApplicationBridge setGrowlDelegate:@""];
|
|
|
|
fm = [NSFileManager defaultManager];
|
|
modifiedSources = [NSMutableArray new];
|
|
modifiedXIBs = [NSMutableArray new];
|
|
ignoredFilePaths = [NSMutableArray new];
|
|
parserPath = [[NSBundle mainBundle] pathForResource:@"parser" ofType:@"j"];
|
|
|
|
if([fm fileExistsAtPath:[@"~/.bash_profile" stringByExpandingTildeInPath]])
|
|
_profilePath = [@"~/.bash_profile" stringByExpandingTildeInPath];
|
|
else if([fm fileExistsAtPath:[@"~/.profile" stringByExpandingTildeInPath]])
|
|
_profilePath = [@"~/.profile" stringByExpandingTildeInPath];
|
|
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)awakeFromNib
|
|
{
|
|
[self registerDefaults];
|
|
[labelCurrentPath setHidden:YES];
|
|
[buttonOpenXCode setEnabled:NO];
|
|
[buttonStop setEnabled:NO];
|
|
[buttonStart setEnabled:YES];
|
|
[spinner setHidden:YES];
|
|
appStartedTimestamp = [NSDate date];
|
|
pathModificationDates = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"pathModificationDates"] mutableCopy];
|
|
lastEventId = [[NSUserDefaults standardUserDefaults] objectForKey:@"lastEventId"];
|
|
|
|
NSBundle *bundle = [NSBundle mainBundle];
|
|
|
|
_iconInactive = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"xcodecapp-cocoa-icon-inactive" ofType:@"png"]];
|
|
_iconActive = [[NSImage alloc] initWithContentsOfFile:[bundle pathForResource:@"xcodecapp-cocoa-icon-active" ofType:@"png"]];
|
|
[_iconActive setSize:NSMakeSize(14.0, 16.0)];
|
|
[_iconInactive setSize:NSMakeSize(14.0, 16.0)];
|
|
|
|
_statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
|
|
[_statusItem setMenu:statusMenu];
|
|
[_statusItem setImage:_iconInactive];
|
|
[_statusItem setHighlightMode:YES];
|
|
[statusMenu setDelegate:self];
|
|
|
|
}
|
|
|
|
- (void)initializeEventStreamWithPath:(NSString*)aPath
|
|
{
|
|
NSArray *pathsToWatch = [NSArray arrayWithObject:aPath];
|
|
void *appPointer = (void *)self;
|
|
FSEventStreamContext context = {0, appPointer, NULL, NULL, NULL};
|
|
NSTimeInterval latency = 3.0;
|
|
|
|
stream = FSEventStreamCreate(NULL, &fsevents_callback, &context, (CFArrayRef) pathsToWatch,
|
|
[lastEventId unsignedLongLongValue], (CFAbsoluteTime) latency,kFSEventStreamCreateFlagUseCFTypes);
|
|
|
|
FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
|
|
FSEventStreamStart(stream);
|
|
}
|
|
|
|
- (void)stopEventStream
|
|
{
|
|
if (stream)
|
|
{
|
|
FSEventStreamStop(stream);
|
|
FSEventStreamInvalidate(stream);
|
|
stream = nil;
|
|
}
|
|
}
|
|
|
|
- (void)registerDefaults
|
|
{
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
NSDictionary *appDefaults = [NSDictionary
|
|
dictionaryWithObjects:[NSArray arrayWithObjects:[NSNumber numberWithUnsignedLongLong:kFSEventStreamEventIdSinceNow], [NSMutableDictionary new], nil]
|
|
forKeys:[NSArray arrayWithObjects:@"lastEventId", @"pathModificationDates", nil]];
|
|
[defaults registerDefaults:appDefaults];
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Notification handlers
|
|
|
|
- (NSApplicationTerminateReply)applicationShouldTerminate: (NSApplication *)app
|
|
{
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
|
|
[defaults setObject:lastEventId forKey:@"lastEventId"];
|
|
[defaults setObject:pathModificationDates forKey:@"pathModificationDates"];
|
|
[defaults synchronize];
|
|
|
|
[self stopEventStream];
|
|
|
|
return NSTerminateNow;
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Utilities
|
|
|
|
- (void)updateLastModificationDateForPath: (NSString *)path
|
|
{
|
|
[pathModificationDates setObject:[NSDate date] forKey:path];
|
|
}
|
|
|
|
- (NSDate*)lastModificationDateForPath: (NSString *)path
|
|
{
|
|
if(nil != [pathModificationDates valueForKey:path])
|
|
return [pathModificationDates valueForKey:path];
|
|
else
|
|
return appStartedTimestamp;
|
|
}
|
|
|
|
- (void)updateLastEventId:(uint64_t)eventId
|
|
{
|
|
lastEventId = [NSNumber numberWithUnsignedLongLong:eventId];
|
|
}
|
|
|
|
- (void)handleFileModification:(NSString*)path ignoreDate:(BOOL)shouldIgnoreDate
|
|
{
|
|
if ([self isPathMatchingIgnoredPaths:path])
|
|
return;
|
|
|
|
NSArray *contents = [fm contentsOfDirectoryAtPath:path error:NULL];
|
|
|
|
for(NSString *node in contents)
|
|
{
|
|
NSString *fullPath = [NSString stringWithFormat:@"%@/%@", path, node];
|
|
NSDictionary *fileAttributes = [fm attributesOfItemAtPath:fullPath error:NULL];
|
|
NSDate *fileModDate = [fileAttributes objectForKey:NSFileModificationDate];
|
|
|
|
if(shouldIgnoreDate || [fileModDate compare:[self lastModificationDateForPath:path]] == NSOrderedDescending)
|
|
{
|
|
if ([self isObjJFile:fullPath])
|
|
[modifiedSources addObject:fullPath];
|
|
if ([self isXIBFile:fullPath])
|
|
{
|
|
[_statusItem setTitle:@"..."];
|
|
NSLog(@"nib2cib %@", fullPath);
|
|
int ret = system([[NSString stringWithFormat:@"source \"%@\"; nib2cib \"%@\";", _profilePath, fullPath] UTF8String]);
|
|
[_statusItem setTitle:@""];
|
|
if (ret == 0)
|
|
{
|
|
if (!shouldIgnoreDate)
|
|
[GrowlApplicationBridge notifyWithTitle:[fullPath lastPathComponent]
|
|
description:@"The XIB file has been converted"
|
|
notificationName:@"DefaultNotifications"
|
|
iconData:nil
|
|
priority:0
|
|
isSticky:NO
|
|
clickContext:nil];
|
|
|
|
}
|
|
else
|
|
NSLog(@"Error in conversion: return code is %d", ret);
|
|
}
|
|
if ([self isObjJFile:fullPath])
|
|
{
|
|
NSString *shadowPath = [[self shadowURLForSourceURL:[NSURL URLWithString:fullPath]] path];
|
|
NSString *command = [NSString stringWithFormat:@"source %@; objj \"%@\" \"%@\" \"%@\";", _profilePath, parserPath, fullPath, shadowPath];
|
|
|
|
NSLog(@"%@", command);
|
|
|
|
[_statusItem setTitle:@"..."];
|
|
int ret = system([command UTF8String]);
|
|
[_statusItem setTitle:@""];
|
|
|
|
if (ret == 0)
|
|
{
|
|
if (!shouldIgnoreDate)
|
|
[GrowlApplicationBridge notifyWithTitle:[fullPath lastPathComponent]
|
|
description:@"The Objective-J file has been converted"
|
|
notificationName:@"DefaultNotifications"
|
|
iconData:nil
|
|
priority:0
|
|
isSticky:NO
|
|
clickContext:nil];
|
|
|
|
}
|
|
else
|
|
NSLog(@"Error in conversion: return code is %d", ret);
|
|
}
|
|
}
|
|
}
|
|
|
|
[self updateLastModificationDateForPath:path];
|
|
}
|
|
|
|
- (BOOL)isObjJFile:(NSString *)path
|
|
{
|
|
return [[[path pathExtension] uppercaseString] isEqual:@"J"];
|
|
}
|
|
|
|
- (BOOL)isXIBFile:(NSString *)path
|
|
{
|
|
return [[[path pathExtension] uppercaseString] isEqual:@"XIB"];
|
|
}
|
|
|
|
- (BOOL)prepareXCodeSupportProject
|
|
{
|
|
XCodeSupportProjectName = [NSString stringWithFormat:@"%@.xcodeproj/", currentProjectName];
|
|
XCodeTemplatePBXPath = [[NSBundle mainBundle] pathForResource:@"project" ofType:@"pbxproj"];
|
|
XCodeSupportFolder = [NSURL URLWithString:@".xCodeSupport/" relativeToURL:currentProjectURL];
|
|
XCodeSupportProject = [NSURL URLWithString:XCodeSupportProjectName relativeToURL:XCodeSupportFolder];
|
|
XCodeSupportProjectSources = [NSURL URLWithString:@"Sources/" relativeToURL:XCodeSupportFolder];
|
|
XCodeSupportPBXPath = [NSString stringWithFormat:@"%@/project.pbxproj", [XCodeSupportProject path]];
|
|
|
|
//[fm removeItemAtURL:XCodeSupportFolder error:nil];
|
|
|
|
// create the template project if it doesn't exist
|
|
if (![fm fileExistsAtPath:[XCodeSupportFolder path]])
|
|
{
|
|
NSLog(@"XCode support folder created at: %@", [XCodeSupportProject path]);
|
|
[fm createDirectoryAtPath:[XCodeSupportProject path] withIntermediateDirectories:YES attributes:nil error:nil];
|
|
|
|
NSLog(@"Copying project.pbxproj from %@ to %@", XCodeTemplatePBXPath, [XCodeSupportProject path]);
|
|
[fm copyItemAtPath:XCodeTemplatePBXPath toPath:XCodeSupportPBXPath error:nil];
|
|
|
|
NSLog(@"Reading the content of the project.pbxproj");
|
|
NSMutableString *PBXContent = [NSMutableString stringWithContentsOfFile:XCodeSupportPBXPath encoding:NSUTF8StringEncoding error:nil];
|
|
[PBXContent replaceOccurrencesOfString:@"${CappuccinoProjectName}"
|
|
withString:currentProjectName
|
|
options:NSCaseInsensitiveSearch
|
|
range:NSMakeRange(0, [PBXContent length])];
|
|
[PBXContent replaceOccurrencesOfString:@"${CappuccinoProjectRelativePath}"
|
|
withString:[currentProjectURL path]
|
|
options:NSCaseInsensitiveSearch
|
|
range:NSMakeRange(0, [PBXContent length])];
|
|
|
|
[PBXContent writeToFile:XCodeSupportPBXPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
|
|
NSLog(@"PBX file adapted to the project");
|
|
|
|
NSLog(@"Creating source folder");
|
|
[fm createDirectoryAtPath:[XCodeSupportProjectSources path] withIntermediateDirectories:YES attributes:nil error:nil];
|
|
|
|
|
|
[NSThread detachNewThreadSelector:@selector(populateXCodeProject:)toTarget:self withObject:nil];
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (void)populateXCodeProject:(id)argement
|
|
{
|
|
[GrowlApplicationBridge notifyWithTitle:[[currentProjectURL path] lastPathComponent]
|
|
description:@"Loading of your project is running..."
|
|
notificationName:@"DefaultNotifications"
|
|
iconData:nil
|
|
priority:0
|
|
isSticky:NO
|
|
clickContext:nil];
|
|
|
|
[self handleFileModification:[NSString stringWithFormat:@"%@", [currentProjectURL path]] ignoreDate:YES];
|
|
NSArray *subdirs = [fm subpathsAtPath:[currentProjectURL path]];
|
|
for (NSString *p in subdirs)
|
|
[self handleFileModification:[NSString stringWithFormat:@"%@/%@", [currentProjectURL path], p] ignoreDate:YES];
|
|
|
|
[labelStatus setStringValue:@"XCodeCapp is running"];
|
|
[buttonOpenXCode setEnabled:YES];
|
|
[buttonStop setEnabled:YES];
|
|
[buttonStart setEnabled:NO];
|
|
[spinner setHidden:YES];
|
|
|
|
[GrowlApplicationBridge notifyWithTitle:[[currentProjectURL path] lastPathComponent]
|
|
description:@"Your project has been loaded successfully!"
|
|
notificationName:@"DefaultNotifications"
|
|
iconData:nil
|
|
priority:0
|
|
isSticky:NO
|
|
clickContext:nil];
|
|
|
|
}
|
|
|
|
- (NSURL*)shadowURLForSourceURL:(NSURL*)aSourceURL
|
|
{
|
|
NSMutableString *flattenedPath = [NSMutableString stringWithString:[aSourceURL path]];
|
|
|
|
[flattenedPath replaceOccurrencesOfString:@"/"
|
|
withString:@"_"
|
|
options:NSCaseInsensitiveSearch
|
|
range:NSMakeRange(0, [[aSourceURL path] length])];
|
|
|
|
NSString *basename = [NSString stringWithFormat:@"%@.h", [flattenedPath stringByDeletingPathExtension]];
|
|
|
|
return [NSURL URLWithString:basename relativeToURL:XCodeSupportProjectSources];
|
|
}
|
|
|
|
- (void)computeIgnoredPaths
|
|
{
|
|
NSString *ignorePath = [NSString stringWithFormat:@"%@/.xcodecapp-ignore", [currentProjectURL path]];
|
|
|
|
if (![fm fileExistsAtPath:ignorePath])
|
|
return;
|
|
|
|
NSString *ignoreFileContent = [NSString stringWithContentsOfFile:ignorePath encoding:NSUTF8StringEncoding error:nil];
|
|
ignoredFilePaths = [NSMutableArray arrayWithArray:[ignoreFileContent componentsSeparatedByString:@"\n"]];
|
|
|
|
NSLog(@"ignored file paths are: %@", ignoredFilePaths);
|
|
}
|
|
|
|
- (BOOL)isPathMatchingIgnoredPaths:(NSString*)aPath
|
|
{
|
|
BOOL isMatching = NO;
|
|
|
|
for (NSString *ignoredPath in ignoredFilePaths)
|
|
{
|
|
if ([ignoredPath isEqual:@""])
|
|
continue;
|
|
|
|
NSMutableString *regexp = [NSMutableString stringWithFormat:@"%@/%@", [currentProjectURL path], ignoredPath];
|
|
[regexp replaceOccurrencesOfString:@"/"
|
|
withString:@"\\/"
|
|
options:NSCaseInsensitiveSearch
|
|
range:NSMakeRange(0, [regexp length])];
|
|
|
|
[regexp replaceOccurrencesOfString:@"."
|
|
withString:@"\\."
|
|
options:NSCaseInsensitiveSearch
|
|
range:NSMakeRange(0, [regexp length])];
|
|
|
|
[regexp replaceOccurrencesOfString:@"*"
|
|
withString:@".*"
|
|
options:NSCaseInsensitiveSearch
|
|
range:NSMakeRange(0, [regexp length])];
|
|
|
|
NSPredicate *regextest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regexp];
|
|
if ([regextest evaluateWithObject:aPath])
|
|
{
|
|
isMatching = YES;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return isMatching;
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Actions
|
|
|
|
- (IBAction)chooseFolder:(id)aSender
|
|
{
|
|
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
|
|
|
|
[openPanel setCanChooseDirectories:YES];
|
|
[openPanel setCanCreateDirectories:YES];
|
|
[openPanel setPrompt:@"Choose Cappuccino project"];
|
|
[openPanel setCanChooseFiles:NO];
|
|
|
|
if ([openPanel runModal] != NSFileHandlingPanelOKButton)
|
|
return;
|
|
|
|
[spinner setHidden:NO];
|
|
[spinner startAnimation:nil];
|
|
|
|
currentProjectURL = [[openPanel URLs] objectAtIndex:0];
|
|
currentProjectName = [[[openPanel URLs] objectAtIndex:0] lastPathComponent];
|
|
|
|
[self computeIgnoredPaths];
|
|
[self initializeEventStreamWithPath:[currentProjectURL path]];
|
|
|
|
BOOL isProjectReady = [self prepareXCodeSupportProject];
|
|
|
|
[labelPath setStringValue:[currentProjectURL path]];
|
|
[labelCurrentPath setHidden:NO];
|
|
[buttonStart setEnabled:NO];
|
|
|
|
if (isProjectReady)
|
|
{
|
|
[spinner setHidden:YES];
|
|
[buttonStop setEnabled:YES];
|
|
[buttonOpenXCode setEnabled:YES];
|
|
[labelStatus setStringValue:@"XCodeCapp is running"];
|
|
}
|
|
|
|
else
|
|
[labelStatus setStringValue:@"XCodeCapp is loading project..."];
|
|
|
|
[_statusItem setImage:_iconActive];
|
|
}
|
|
|
|
- (IBAction)stopListener:(id)aSender
|
|
{
|
|
currentProjectURL = nil;
|
|
currentProjectName = nil;
|
|
[ignoredFilePaths removeAllObjects];
|
|
[labelPath setStringValue:@""];
|
|
[labelCurrentPath setHidden:YES];
|
|
[buttonOpenXCode setEnabled:NO];
|
|
[buttonStop setEnabled:NO];
|
|
[buttonStart setEnabled:YES];
|
|
[labelStatus setStringValue:@"XCodeCapp is not running"];
|
|
[self stopEventStream];
|
|
[_statusItem setImage:_iconInactive];
|
|
}
|
|
|
|
- (IBAction)openXCode:(id)aSender
|
|
{
|
|
if (!currentProjectURL)
|
|
return;
|
|
|
|
system([[NSString stringWithFormat:@"open \"%@\"", [XCodeSupportProject path]] UTF8String]);
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Delegates
|
|
|
|
- (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag
|
|
{
|
|
[mainWindow makeKeyAndOrderFront:nil];
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)validateMenuItem:(NSMenuItem*)aMenuItem
|
|
{
|
|
if (aMenuItem == menuItemStart)
|
|
return !currentProjectURL;
|
|
if ((aMenuItem == menuItemStop) || (aMenuItem == menuItemOpenXCode))
|
|
return !!currentProjectURL;
|
|
|
|
return YES;
|
|
}
|
|
|
|
@end
|