New: XcodeCapp 3 total overhaul part 2.

Whew, a complete overhaul of my previous complete overhaul. Changes in no particular order:

- Redesigned the About box.
- Converted tabs to spaces.
- Using Lumberjack for (much better) logging. When debugging, copious logging goes only to Xcode's console, not to the system log. In the release build, only basic info and errors goes to the system log.
- Reformatted the help using Pages, it's a PDF now (so I can use better fonts).
- Massive optimization across the board. All time-consuming operations are done with NSOperation, which uses Grand Central Dispatch, and can be interrupted. So the XCC menu remains fully responsive during source processing.
- Updates to the Xcode project are coalesced so only one read/update/write operation is done.
- Instead of launching a shell and reading the .profile, the target executable is directly launched, but it must reside somewhere in the default binary path + /usr/local/bin:/usr/local/narwhal/bin:~/bin. If jsc, python, objj and nib2cib cannot be found in that path, the user is alerted and XCC quits.
- Added a preference (true by default) to automatically load the Xcode project when a project is opened.
- Added a preference to turn off per-file processing notifications.
- Added a hidden preference to set the log level. Useful for debugging a user's release build.
- Redesigned the Preferences window.
- Renamed some methods/properties, eliminated some unused properties.
- All of the windows remember their position.
- Moved the fsevent_callback into XcodeCapp.m.
- Fixed a bug in mod_pbxproj.py not setting the type of PBXFileReferences which are directories to "folder".
- Added support for categories in parser.j. Woo-hoo!
- Rewrote pbxprojModifier.py to use a class.
- Renamed the Xcode project group names to "Cocoa Classes" and "Cappuccino Source". Within "Cappuccino Source", framework code is kept in a "Frameworks" group.
- Shadow filenames are all project-relative now, thus *much* shorter.
- pbxprojModifier.py keeps the groups and files in the project in sorted order: Resource folders are at the top, followed by Cocoa Classes, followed by Cappuccino Source. Within Cocoa Classes, non-framework files come first, followed by framework files, followed by xcc_general_include.h.
- If either .XcodeSupport or the .xcodeproj is missing, the other is regenerated to ensure they stay in sync.
- A compatibility version for .XcodeSupport is stored inside it in Info.plist. If that version < the app's compatibility version, the project is reset. This ensures that format changes in the future will not result in projects in an unknown state.
- If the Xcode project cannot be opened, the user is alerted and given the option of regenerating the project.
- Resetting the project deletes all .cibs to force the .xibs to be regenerated.
- All possible FSEvents are dealt with individually now, and in a way that (hopefully) maintains sync between Cappuccino and XCC.
- A file descriptor to the Xcode project is kept open so that If the project path changes it can be relocated. If it does move, the user is alerted ad given the option of reloading the project or quitting.
- If one of the watched paths changes, the project is reloaded.
- Xcode creates temporary files, they are properly filtered out now when handling FSEvents.
- NSRegularExpression was being used before, but that is OS X 10.7 only. NSPredicate is used instead now.
-
This commit is contained in:
Aparajita Fishman
2013-05-02 17:04:46 -04:00
parent 6de967345c
commit 550401dffe
39 changed files with 3525 additions and 2772 deletions
+175 -136
View File
@@ -20,24 +20,22 @@
#import <AppKit/NSApplication.h>
#import "AppController.h"
#import "TNXcodeCapp.h"
#import "Notifications.h"
#import "XcodeCapp.h"
#import "UserDefaults.h"
#include "macros.h"
AppController *SharedAppControllerInstance = nil;
@interface AppController ()
@property BOOL supportsFileModeListening;
@property (nonatomic) NSImage *iconActive;
@property (nonatomic) NSImage *iconInactive;
@property (nonatomic) NSImage *iconWorking;
@property (nonatomic) NSImage *iconError;
@property (nonatomic) NSMenu *recentMenu;
@property NSStatusItem *statusItem;
@property (nonatomic) NSImage *iconActive;
@property (nonatomic) NSImage *iconInactive;
@property (nonatomic) NSImage *iconWorking;
@property (nonatomic) NSImage *iconError;
@property (nonatomic) NSMenu *recentMenu;
@property NSStatusItem *statusItem;
@end
@@ -52,18 +50,22 @@ AppController *SharedAppControllerInstance = nil;
#pragma mark - Initialization
- (void)awakeFromNib
{
{
SharedAppControllerInstance = self;
[self registerDefaults];
[self registerDefaultPreferences];
[self initLogging];
DDLogVerbose(@"\n******************************\n** XcodeCapp started **\n******************************\n");
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
self.statusItem.menu = self.statusMenu;
self.statusItem.image = self.iconInactive;
self.statusItem.highlightMode = YES;
self.statusItem.length = self.iconInactive.size.width + 12; // Add some space around the icon
self.statusMenu.delegate = self;
self.helpTextView.textContainerInset = NSMakeSize(10.0, 10.0);
self.aboutWindow.backgroundColor = [NSColor whiteColor];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
@@ -83,10 +85,9 @@ AppController *SharedAppControllerInstance = nil;
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self selector:@selector(XcodeCappConversionDidStart:) name:XCCConversionDidStartNotification object:self.xcc];
[defaultCenter addObserver:self selector:@selector(XcodeCappConversionDidStop:) name:XCCConversionDidStopNotification object:self.xcc];
[defaultCenter addObserver:self selector:@selector(XcodeCappDidPopulateProject:) name:XCCDidPopulateProjectNotification object:self.xcc];
[defaultCenter addObserver:self selector:@selector(XcodeCappListeningDidStart:) name:XCCListeningDidStartNotification object:self.xcc];
[defaultCenter addObserver:self selector:@selector(batchDidStart:) name:XCCBatchDidStartNotification object:nil];
[defaultCenter addObserver:self selector:@selector(batchDidEnd:) name:XCCBatchDidEndNotification object:nil];
[defaultCenter addObserver:self selector:@selector(projectDidFinishLoading:) name:XCCProjectDidFinishLoadingNotification object:nil];
[self pruneProjectHistory];
[self updateHistoryMenu];
@@ -94,37 +95,77 @@ AppController *SharedAppControllerInstance = nil;
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
[self.xcc start];
if (![self.xcc executablesAreAccessible])
{
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
NSRunAlertPanel(@"Executables are missing.", @"Please make sure that python, jsc, objj and nib2cib (or symlinks to them) are somewhere within one of these directories:\n\n/bin\n/usr/bin\n/usr/local/bin\n/usr/local/narwhal/bin\n~/bin\n\nThen launch XcodeCapp again.", @"Quit", nil, nil);
[[NSApplication sharedApplication] terminate:self];
return;
}
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if (![defaults boolForKey:kDefaultXCCReopenLastProject])
return;
NSString *lastOpenedPath = [defaults objectForKey:kDefaultLastOpenedPath];
if (lastOpenedPath)
{
if ([[NSFileManager defaultManager] fileExistsAtPath:lastOpenedPath])
[self loadProjectAtPath:lastOpenedPath];
else
[defaults removeObjectForKey:kDefaultLastOpenedPath];
}
}
/*!
Register default values for preferences
Register default values for preferences
*/
- (void)registerDefaults
- (void)registerDefaultPreferences
{
NSDictionary *appDefaults = @{
kDefaultLastEventId: [NSNumber numberWithUnsignedLongLong:kFSEventStreamEventIdSinceNow],
kDefaultFirstLaunch: @YES,
kDefaultFirstLaunchVersion: @2.0,
kDefaultXCCAPIMode: [NSNumber numberWithInt:kXCCAPIModeAuto],
kDefaultXCCReactMode: @YES,
kDefaultXCCReopenLastProject: @YES,
kDefaultXCCAutoOpenErrorsPanelOnWarnings: @YES,
kDefaultXCCAutoOpenErrorsPanelOnErrors: @YES,
kDefaultXCCProjectHistory: [NSArray new],
kDefaultMaxRecentProjects: @20
kDefaultLastEventId: [NSNumber numberWithUnsignedLongLong:kFSEventStreamEventIdSinceNow],
kDefaultFirstLaunch: @YES,
kDefaultFirstLaunchVersion: @2.0,
kDefaultXCCAPIMode: [NSNumber numberWithInt:kXCCAPIModeAuto],
kDefaultXCCReactToInodeMod: @YES,
kDefaultXCCReopenLastProject: @YES,
kDefaultXCCAutoOpenErrorsPanelOnWarnings: @YES,
kDefaultXCCAutoOpenErrorsPanelOnErrors: @YES,
kDefaultXCCProjectHistory: [NSArray new],
kDefaultMaxRecentProjects: @20,
kDefaultLogLevel: [NSNumber numberWithInt:LOG_LEVEL_WARN],
kDefaultAutoOpenXcodeProject: @YES,
kDefaultShowProcessingNotices: @YES
};
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults registerDefaults:appDefaults];
[defaults registerDefaults:appDefaults];
[defaults addObserver:self
[defaults addObserver:self
forKeyPath:kDefaultMaxRecentProjects
options:NSKeyValueObservingOptionNew
context:NULL];
}
- (void)initLogging
{
#if DEBUG
[DDLog addLogger:[DDTTYLogger sharedInstance]];
[[DDTTYLogger sharedInstance] setColorsEnabled:YES];
[DDLogLevel setLogLevel:LOG_LEVEL_VERBOSE];
#else
[DDLog addLogger:[DDASLLogger sharedInstance]];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[DDLogLevel setLogLevel:(int)[defaults integerForKey:kDefaultLogLevel]];
#endif
}
- (void)pruneProjectHistory
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
@@ -134,7 +175,7 @@ AppController *SharedAppControllerInstance = nil;
for (NSInteger i = projectHistory.count - 1; i >= 0; --i)
{
if (![fm fileExistsAtPath:projectHistory[i]])
[projectHistory removeObjectAtIndex:i];
[projectHistory removeObjectAtIndex:i];
}
NSInteger maxProjects = [defaults integerForKey:kDefaultMaxRecentProjects];
@@ -186,77 +227,45 @@ AppController *SharedAppControllerInstance = nil;
_recentMenu = [NSMenu new];
_recentMenu.delegate = self;
self.menuItemHistory.submenu = _recentMenu;
}
}
return _recentMenu;
}
#pragma mark - Notification handlers
/*!
Clean up when the application stops.
It will stop the FSEvent listener, and store the last event id.
*/
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)app
- (void)batchDidStart:(NSNotification *)note
{
[self.xcc stop];
DDLogVerbose(@"Batch start");
return NSTerminateNow;
}
- (void)XcodeCappConversionDidStart:(NSNotification *)aNotification
{
self.statusItem.image = self.iconWorking;
}
- (void)XcodeCappConversionDidStop:(NSNotification *)aNotification
- (void)batchDidEnd:(NSNotification *)note
{
DDLogVerbose(@"Batch end");
if (!self.xcc.isLoadingProject)
self.statusItem.image = self.xcc.hasErrors ? self.iconError : self.iconActive;
}
- (void)XcodeCappDidPopulateProject:(NSNotification *)aNotification
{
}
- (void)XcodeCappListeningDidStart:(NSNotification *)aNotification
- (void)projectDidFinishLoading:(NSNotification *)note
{
self.statusItem.image = self.xcc.hasErrors ? self.iconError : self.iconActive;
self.menuItemListen.title = [NSString stringWithFormat:@"Close “%@”", self.xcc.currentProjectPath.lastPathComponent];
self.menuItemListen.action = @selector(stopListening:);
self.menuItemListen.title = [NSString stringWithFormat:@"Close “%@”", self.xcc.projectPath.lastPathComponent];
self.menuItemListen.action = @selector(closeProject:);
}
// Watch changes to the max recent projects preference
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:kDefaultMaxRecentProjects])
if ([keyPath isEqualToString:kDefaultMaxRecentProjects])
[self pruneProjectHistory];
}
#pragma mark - Utilities
- (void)updateHistoryMenu
{
[self.recentMenu removeAllItems];
NSArray *projectHistory = [[NSUserDefaults standardUserDefaults] arrayForKey:kDefaultXCCProjectHistory];
for (NSString *path in projectHistory)
{
NSMenuItem *item = [self.recentMenu addItemWithTitle:path.lastPathComponent action:@selector(switchToProject:) keyEquivalent:@""];
[item setEnabled:YES];
item.representedObject = path;
}
[self.recentMenu addItem:[NSMenuItem separatorItem]];
[self.recentMenu addItemWithTitle:@"Clear history" action:@selector(clearProjectHistory:) keyEquivalent:@""];
self.menuItemHistory.enabled = [projectHistory count] > 0;
}
#pragma mark - Actions
- (IBAction)listenToProject:(id)aSender
- (IBAction)loadProject:(id)aSender
{
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
@@ -270,57 +279,23 @@ AppController *SharedAppControllerInstance = nil;
return;
NSString *projectPath = [[openPanel.URLs[0] path] stringByStandardizingPath];
[self listenToProjectAtPath:projectPath];
[self loadProjectAtPath:projectPath];
}
- (void)listenToProjectAtPath:(NSString *)path
{
[self stopListening:self];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *projectHistory = [[defaults arrayForKey:kDefaultXCCProjectHistory] mutableCopy];
if ([projectHistory containsObject:path])
[projectHistory removeObject:path];
// The path may no longer be there, validate it
NSFileManager *fm = [NSFileManager defaultManager];
BOOL exists, isDirectory;
exists = [fm fileExistsAtPath:path isDirectory:&isDirectory];
if (exists && isDirectory)
{
[projectHistory insertObject:path atIndex:0];
}
else
{
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
NSRunAlertPanel(@"Project not found.", @"%@ %@", nil, nil, nil, path, !exists ? @"no longer exists." : @"is not a directory.");
}
[defaults setObject:projectHistory forKey:kDefaultXCCProjectHistory];
[self pruneProjectHistory];
[self updateHistoryMenu];
if (exists && isDirectory)
[self.xcc listenToProjectAtPath:path];
}
- (void)stopListening:(id)aSender
- (void)closeProject:(id)aSender
{
[self.xcc stop];
self.statusItem.image = self.iconInactive;
self.menuItemListen.title = @"Open Project…";
self.menuItemListen.action = @selector(listenToProject:);
self.menuItemListen.action = @selector(loadProject:);
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kDefaultLastOpenedPath];
}
- (void)switchToProject:(id)aSender
- (void)switchToProject:(NSMenuItem *)aSender
{
[self listenToProjectAtPath:[aSender representedObject]];
[self loadProjectAtPath:aSender.representedObject];
}
- (void)clearProjectHistory:(id)aSender
@@ -331,41 +306,42 @@ AppController *SharedAppControllerInstance = nil;
- (IBAction)openHelp:(id)aSender
{
[self.helpTextView readRTFDFromFile:[[NSBundle mainBundle] pathForResource:@"help" ofType:@"rtfd"]];
[self openWindow:self.helpWindow centered:YES];
if (!self.helpView.document)
{
NSURL *helpURL = [[NSBundle mainBundle] URLForResource:@"help" withExtension:@"pdf"];
PDFDocument *help = [[PDFDocument alloc] initWithURL:helpURL];
self.helpView.document = help;
}
[self openWindow:self.helpWindow];
}
- (IBAction)openAbout:(id)aSender
{
[self openWindow:self.aboutWindow centered:YES];
[self openWindow:self.aboutWindow];
}
- (IBAction)openPreferences:(id)aSender
{
[self openWindow:self.preferencesWindow centered:NO];
[self openWindow:self.preferencesWindow];
}
- (void)openWindow:(NSWindow *)aWindow centered:(BOOL)shouldBeCentered
{
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
if (shouldBeCentered)
[aWindow center];
[aWindow makeKeyAndOrderFront:nil];
}
#pragma mark - Delegates
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)app
{
[self.xcc stop];
return NSTerminateNow;
}
- (BOOL)validateMenuItem:(NSMenuItem *)aMenuItem
{
NSMenu *menu = aMenuItem.menu;
if (aMenuItem == self.menuItemListen)
{
return !!self.xcc.currentProjectPath;
return !!self.xcc.projectPath;
}
else if (menu == self.recentMenu)
{
@@ -385,11 +361,74 @@ AppController *SharedAppControllerInstance = nil;
return YES;
}
#pragma mark - Private Helpers
#pragma mark - Bindings
- (NSString *)bundleVersion
{
return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
}
#pragma mark - Private Helpers
- (void)loadProjectAtPath:(NSString *)path
{
if ([self.xcc.projectPath isEqualToString:path])
return;
[self closeProject:self];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *projectHistory = [[defaults arrayForKey:kDefaultXCCProjectHistory] mutableCopy];
if ([projectHistory containsObject:path])
[projectHistory removeObject:path];
// The path may no longer be there, validate it
NSFileManager *fm = [NSFileManager defaultManager];
BOOL exists, isDirectory;
exists = [fm fileExistsAtPath:path isDirectory:&isDirectory];
if (exists && isDirectory)
{
[projectHistory insertObject:path atIndex:0];
}
else
{
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
NSRunAlertPanel(@"Project not found.", @"%@ %@", nil, nil, nil, path, !exists ? @"no longer exists." : @"is not a directory.");
}
[defaults setObject:projectHistory forKey:kDefaultXCCProjectHistory];
[self pruneProjectHistory];
[self updateHistoryMenu];
if (exists && isDirectory)
[self.xcc loadProjectAtPath:path];
}
- (void)openWindow:(NSWindow *)aWindow
{
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
[aWindow makeKeyAndOrderFront:nil];
}
- (void)updateHistoryMenu
{
[self.recentMenu removeAllItems];
NSArray *projectHistory = [[NSUserDefaults standardUserDefaults] arrayForKey:kDefaultXCCProjectHistory];
for (NSString *path in projectHistory)
{
NSMenuItem *item = [self.recentMenu addItemWithTitle:path.lastPathComponent action:@selector(switchToProject:) keyEquivalent:@""];
[item setEnabled:YES];
item.representedObject = path;
}
[self.recentMenu addItem:[NSMenuItem separatorItem]];
[self.recentMenu addItemWithTitle:@"Clear history" action:@selector(clearProjectHistory:) keyEquivalent:@""];
self.menuItemHistory.enabled = [projectHistory count] > 0;
}
@end