diff --git a/Tools/XcodeCapp/XcodeCapp.xcodeproj/project.pbxproj b/Tools/XcodeCapp/XcodeCapp.xcodeproj/project.pbxproj index 420f6d056..5a46a21d7 100644 --- a/Tools/XcodeCapp/XcodeCapp.xcodeproj/project.pbxproj +++ b/Tools/XcodeCapp/XcodeCapp.xcodeproj/project.pbxproj @@ -7,17 +7,17 @@ objects = { /* Begin PBXBuildFile section */ + E100444B172E25690062E953 /* DDLog.m in Sources */ = {isa = PBXBuildFile; fileRef = E1004448172E25690062E953 /* DDLog.m */; }; + E100444C172E25690062E953 /* DDTTYLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = E100444A172E25690062E953 /* DDTTYLogger.m */; }; E10E4CD717298E5F004AB09E /* project.pbxproj in Resources */ = {isa = PBXBuildFile; fileRef = E164FE2A172188F300263CE3 /* project.pbxproj */; }; - E132ED951724B64C00D870AE /* FSEventCallback.m in Sources */ = {isa = PBXBuildFile; fileRef = E132ED941724B64C00D870AE /* FSEventCallback.m */; }; E164FDD31720E77100263CE3 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E164FDD21720E77100263CE3 /* Cocoa.framework */; }; E164FDDF1720E77100263CE3 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E164FDDE1720E77100263CE3 /* main.m */; }; E164FDE91720E77100263CE3 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = E164FDE71720E77100263CE3 /* MainMenu.xib */; }; E164FDF51720EBC100263CE3 /* AppController.m in Sources */ = {isa = PBXBuildFile; fileRef = E164FDF41720EBC100263CE3 /* AppController.m */; }; E164FDF71720EBD600263CE3 /* Growl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E164FDF61720EBD600263CE3 /* Growl.framework */; }; - E164FDFF1720EC5B00263CE3 /* TNXcodeCapp.m in Sources */ = {isa = PBXBuildFile; fileRef = E164FDFB1720EC5B00263CE3 /* TNXcodeCapp.m */; }; + E164FDFF1720EC5B00263CE3 /* XcodeCapp.m in Sources */ = {isa = PBXBuildFile; fileRef = E164FDFB1720EC5B00263CE3 /* XcodeCapp.m */; }; E164FE001720EC5B00263CE3 /* UserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = E164FDFD1720EC5B00263CE3 /* UserDefaults.m */; }; E164FE061720ED4500263CE3 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = E164FE041720ED4400263CE3 /* InfoPlist.strings */; }; - E164FE0D1720F26C00263CE3 /* help.rtfd in Resources */ = {isa = PBXBuildFile; fileRef = E164FE081720F26C00263CE3 /* help.rtfd */; }; E164FE181720F44B00263CE3 /* parser.j in Resources */ = {isa = PBXBuildFile; fileRef = E164FE171720F44B00263CE3 /* parser.j */; }; E164FE1A1720F49A00263CE3 /* Growl Registration Ticket.growlRegDict in Resources */ = {isa = PBXBuildFile; fileRef = E164FE191720F49A00263CE3 /* Growl Registration Ticket.growlRegDict */; }; E164FE1F1720F51E00263CE3 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E164FE1D1720F50100263CE3 /* CoreServices.framework */; }; @@ -34,6 +34,12 @@ E170C40617298833003DC489 /* icon-inactive@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = E170C40117298833003DC489 /* icon-inactive@2x.png */; }; E170C40717298833003DC489 /* icon-working@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = E170C40217298833003DC489 /* icon-working@2x.png */; }; E17A822D172700B90095CD83 /* XcodeCapp.iconset in Resources */ = {isa = PBXBuildFile; fileRef = E17A822C172700B90095CD83 /* XcodeCapp.iconset */; }; + E17B55411732F57700809FFB /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E17B55401732F57700809FFB /* Quartz.framework */; }; + E17B55431732F72700809FFB /* help.pdf in Resources */ = {isa = PBXBuildFile; fileRef = E17B55421732F72700809FFB /* help.pdf */; }; + E1A5594A172C5EE20088FB61 /* FindSourceFilesOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = E1A55949172C5EE20088FB61 /* FindSourceFilesOperation.m */; }; + E1A5594D172CA8D40088FB61 /* ProcessSourceOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = E1A5594C172CA8D40088FB61 /* ProcessSourceOperation.m */; }; + E1A55954172CBA8E0088FB61 /* Notifications.m in Sources */ = {isa = PBXBuildFile; fileRef = E1A55953172CBA8E0088FB61 /* Notifications.m */; }; + E1E167F1172F3B20003198BA /* DDLogLevel.m in Sources */ = {isa = PBXBuildFile; fileRef = E1E167F0172F3B20003198BA /* DDLogLevel.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -62,9 +68,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - E132ED941724B64C00D870AE /* FSEventCallback.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSEventCallback.m; sourceTree = ""; }; - E132ED961724B6C400D870AE /* FSEventCallback.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSEventCallback.h; sourceTree = ""; }; - E132ED991724B71600D870AE /* TNXCodeCapp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNXCodeCapp.h; sourceTree = ""; }; + E1004447172E25690062E953 /* DDLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; name = DDLog.h; path = ../../../../../Cocoa/CocoaLumberjack/Lumberjack/DDLog.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + E1004448172E25690062E953 /* DDLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; name = DDLog.m; path = ../../../../../Cocoa/CocoaLumberjack/Lumberjack/DDLog.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + E1004449172E25690062E953 /* DDTTYLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; name = DDTTYLogger.h; path = ../../../../../Cocoa/CocoaLumberjack/Lumberjack/DDTTYLogger.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + E100444A172E25690062E953 /* DDTTYLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DDTTYLogger.m; path = ../../../../../Cocoa/CocoaLumberjack/Lumberjack/DDTTYLogger.m; sourceTree = ""; }; + E132ED991724B71600D870AE /* XcodeCapp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XcodeCapp.h; sourceTree = ""; }; E132ED9A1724B71D00D870AE /* AppController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppController.h; sourceTree = ""; }; E164FDCF1720E77100263CE3 /* XcodeCapp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XcodeCapp.app; sourceTree = BUILT_PRODUCTS_DIR; }; E164FDD21720E77100263CE3 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; @@ -77,18 +85,17 @@ E164FDE81720E77100263CE3 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; E164FDEF1720E7FA00263CE3 /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; E164FDF01720E7FA00263CE3 /* Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - E164FDF41720EBC100263CE3 /* AppController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppController.m; sourceTree = ""; }; + E164FDF41720EBC100263CE3 /* AppController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AppController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; E164FDF61720EBD600263CE3 /* Growl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Growl.framework; path = XcodeCapp/Growl.framework; sourceTree = SOURCE_ROOT; }; - E164FDFB1720EC5B00263CE3 /* TNXcodeCapp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNXcodeCapp.m; sourceTree = ""; }; - E164FDFD1720EC5B00263CE3 /* UserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserDefaults.m; sourceTree = ""; }; + E164FDFB1720EC5B00263CE3 /* XcodeCapp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = XcodeCapp.m; sourceTree = ""; usesTabs = 0; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + E164FDFD1720EC5B00263CE3 /* UserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = UserDefaults.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; E164FE051720ED4500263CE3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = XcodeCapp/en.lproj/InfoPlist.strings; sourceTree = ""; }; - E164FE081720F26C00263CE3 /* help.rtfd */ = {isa = PBXFileReference; lastKnownFileType = wrapper.rtfd; name = help.rtfd; path = Resources/help.rtfd; sourceTree = ""; }; E164FE131720F40400263CE3 /* mod_pbxproj.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = mod_pbxproj.py; sourceTree = ""; }; E164FE141720F40400263CE3 /* pbxprojModifier.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = pbxprojModifier.py; sourceTree = ""; }; E164FE171720F44B00263CE3 /* parser.j */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = parser.j; sourceTree = ""; }; E164FE191720F49A00263CE3 /* Growl Registration Ticket.growlRegDict */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "Growl Registration Ticket.growlRegDict"; sourceTree = ""; }; E164FE1D1720F50100263CE3 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; - E164FE2A172188F300263CE3 /* project.pbxproj */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.pbxproject; name = project.pbxproj; path = Resources/project.pbxproj; sourceTree = ""; }; + E164FE2A172188F300263CE3 /* project.pbxproj */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.pbxproject; lineEnding = 0; name = project.pbxproj; path = Resources/project.pbxproj; sourceTree = ""; }; E164FE2C1721A34900263CE3 /* icon-active.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "icon-active.png"; path = "Resources/icon-active.png"; sourceTree = ""; }; E164FE2D1721A34900263CE3 /* icon-inactive.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "icon-inactive.png"; path = "Resources/icon-inactive.png"; sourceTree = ""; }; E164FE2E1721A34900263CE3 /* icon-working.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "icon-working.png"; path = "Resources/icon-working.png"; sourceTree = ""; }; @@ -98,7 +105,17 @@ E170C40117298833003DC489 /* icon-inactive@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "icon-inactive@2x.png"; path = "Resources/icon-inactive@2x.png"; sourceTree = ""; }; E170C40217298833003DC489 /* icon-working@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "icon-working@2x.png"; path = "Resources/icon-working@2x.png"; sourceTree = ""; }; E17A822C172700B90095CD83 /* XcodeCapp.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; name = XcodeCapp.iconset; path = Resources/XcodeCapp.iconset; sourceTree = ""; }; - E1DD4DF517282BFC0063C0D7 /* UserDefaults.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UserDefaults.h; sourceTree = ""; }; + E17B55401732F57700809FFB /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; }; + E17B55421732F72700809FFB /* help.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; name = help.pdf; path = Resources/help.pdf; sourceTree = ""; }; + E1A55948172C5EE20088FB61 /* FindSourceFilesOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = FindSourceFilesOperation.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + E1A55949172C5EE20088FB61 /* FindSourceFilesOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = FindSourceFilesOperation.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + E1A5594B172CA8D40088FB61 /* ProcessSourceOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProcessSourceOperation.h; sourceTree = ""; }; + E1A5594C172CA8D40088FB61 /* ProcessSourceOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ProcessSourceOperation.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + E1A55952172CBA6E0088FB61 /* Notifications.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Notifications.h; sourceTree = ""; }; + E1A55953172CBA8E0088FB61 /* Notifications.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Notifications.m; sourceTree = ""; }; + E1DD4DF517282BFC0063C0D7 /* UserDefaults.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = UserDefaults.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + E1E167EF172F3B20003198BA /* DDLogLevel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = DDLogLevel.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + E1E167F0172F3B20003198BA /* DDLogLevel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = DDLogLevel.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -108,6 +125,7 @@ files = ( E164FDD31720E77100263CE3 /* Cocoa.framework in Frameworks */, E164FE1F1720F51E00263CE3 /* CoreServices.framework in Frameworks */, + E17B55411732F57700809FFB /* Quartz.framework in Frameworks */, E164FDF71720EBD600263CE3 /* Growl.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -115,6 +133,17 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + E1004446172E252C0062E953 /* Lumberjack (logging) */ = { + isa = PBXGroup; + children = ( + E1004447172E25690062E953 /* DDLog.h */, + E1004448172E25690062E953 /* DDLog.m */, + E1004449172E25690062E953 /* DDTTYLogger.h */, + E100444A172E25690062E953 /* DDTTYLogger.m */, + ); + name = "Lumberjack (logging)"; + sourceTree = ""; + }; E164FDC61720E77100263CE3 = { isa = PBXGroup; children = ( @@ -149,6 +178,7 @@ E164FDD51720E77100263CE3 /* AppKit.framework */, E164FDD61720E77100263CE3 /* CoreData.framework */, E164FDD71720E77100263CE3 /* Foundation.framework */, + E17B55401732F57700809FFB /* Quartz.framework */, ); name = "Other Frameworks"; sourceTree = ""; @@ -158,15 +188,17 @@ children = ( E164FDF41720EBC100263CE3 /* AppController.m */, E132ED9A1724B71D00D870AE /* AppController.h */, - E164FDFB1720EC5B00263CE3 /* TNXcodeCapp.m */, - E132ED991724B71600D870AE /* TNXCodeCapp.h */, - E132ED941724B64C00D870AE /* FSEventCallback.m */, - E132ED961724B6C400D870AE /* FSEventCallback.h */, + E164FDFB1720EC5B00263CE3 /* XcodeCapp.m */, + E132ED991724B71600D870AE /* XcodeCapp.h */, + E1A55947172C5EA80088FB61 /* Operations */, E164FDFD1720EC5B00263CE3 /* UserDefaults.m */, E1DD4DF517282BFC0063C0D7 /* UserDefaults.h */, - E164FDEF1720E7FA00263CE3 /* Debug.xcconfig */, - E164FDF01720E7FA00263CE3 /* Release.xcconfig */, + E1A55952172CBA6E0088FB61 /* Notifications.h */, + E1A55953172CBA8E0088FB61 /* Notifications.m */, + E1E167EF172F3B20003198BA /* DDLogLevel.h */, + E1E167F0172F3B20003198BA /* DDLogLevel.m */, E164FE121720F3E100263CE3 /* Scripts */, + E1004446172E252C0062E953 /* Lumberjack (logging) */, E164FE071720F25400263CE3 /* Resources */, E164FDD91720E77100263CE3 /* Supporting Files */, ); @@ -180,6 +212,8 @@ E164FE041720ED4400263CE3 /* InfoPlist.strings */, E164FDDE1720E77100263CE3 /* main.m */, E164FDE01720E77100263CE3 /* XcodeCapp-Prefix.pch */, + E164FDEF1720E7FA00263CE3 /* Debug.xcconfig */, + E164FDF01720E7FA00263CE3 /* Release.xcconfig */, ); name = "Supporting Files"; sourceTree = ""; @@ -187,6 +221,7 @@ E164FE071720F25400263CE3 /* Resources */ = { isa = PBXGroup; children = ( + E164FDE71720E77100263CE3 /* MainMenu.xib */, E164FE2D1721A34900263CE3 /* icon-inactive.png */, E170C40117298833003DC489 /* icon-inactive@2x.png */, E164FE2E1721A34900263CE3 /* icon-working.png */, @@ -196,10 +231,9 @@ E170C3FF17298833003DC489 /* icon-error.png */, E170C40017298833003DC489 /* icon-error@2x.png */, E17A822C172700B90095CD83 /* XcodeCapp.iconset */, - E164FDE71720E77100263CE3 /* MainMenu.xib */, + E17B55421732F72700809FFB /* help.pdf */, E164FE2A172188F300263CE3 /* project.pbxproj */, E164FE191720F49A00263CE3 /* Growl Registration Ticket.growlRegDict */, - E164FE081720F26C00263CE3 /* help.rtfd */, ); name = Resources; sourceTree = ""; @@ -214,6 +248,17 @@ path = Scripts; sourceTree = ""; }; + E1A55947172C5EA80088FB61 /* Operations */ = { + isa = PBXGroup; + children = ( + E1A55949172C5EE20088FB61 /* FindSourceFilesOperation.m */, + E1A55948172C5EE20088FB61 /* FindSourceFilesOperation.h */, + E1A5594C172CA8D40088FB61 /* ProcessSourceOperation.m */, + E1A5594B172CA8D40088FB61 /* ProcessSourceOperation.h */, + ); + name = Operations; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -269,7 +314,6 @@ files = ( E164FDE91720E77100263CE3 /* MainMenu.xib in Resources */, E164FE061720ED4500263CE3 /* InfoPlist.strings in Resources */, - E164FE0D1720F26C00263CE3 /* help.rtfd in Resources */, E10E4CD717298E5F004AB09E /* project.pbxproj in Resources */, E164FE181720F44B00263CE3 /* parser.j in Resources */, E164FE1A1720F49A00263CE3 /* Growl Registration Ticket.growlRegDict in Resources */, @@ -282,6 +326,7 @@ E170C40517298833003DC489 /* icon-error@2x.png in Resources */, E170C40617298833003DC489 /* icon-inactive@2x.png in Resources */, E170C40717298833003DC489 /* icon-working@2x.png in Resources */, + E17B55431732F72700809FFB /* help.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -294,9 +339,14 @@ files = ( E164FDDF1720E77100263CE3 /* main.m in Sources */, E164FDF51720EBC100263CE3 /* AppController.m in Sources */, - E164FDFF1720EC5B00263CE3 /* TNXcodeCapp.m in Sources */, + E164FDFF1720EC5B00263CE3 /* XcodeCapp.m in Sources */, E164FE001720EC5B00263CE3 /* UserDefaults.m in Sources */, - E132ED951724B64C00D870AE /* FSEventCallback.m in Sources */, + E1A5594A172C5EE20088FB61 /* FindSourceFilesOperation.m in Sources */, + E1A5594D172CA8D40088FB61 /* ProcessSourceOperation.m in Sources */, + E1A55954172CBA8E0088FB61 /* Notifications.m in Sources */, + E100444B172E25690062E953 /* DDLog.m in Sources */, + E100444C172E25690062E953 /* DDTTYLogger.m in Sources */, + E1E167F1172F3B20003198BA /* DDLogLevel.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Tools/XcodeCapp/XcodeCapp/AppController.h b/Tools/XcodeCapp/XcodeCapp/AppController.h index d994293a2..96cea4703 100644 --- a/Tools/XcodeCapp/XcodeCapp/AppController.h +++ b/Tools/XcodeCapp/XcodeCapp/AppController.h @@ -18,8 +18,9 @@ #import #import +#import -#import "TNXcodeCapp.h" +@class XcodeCapp; @interface AppController : NSObject @@ -32,16 +33,18 @@ @property (strong) IBOutlet NSWindow *preferencesWindow; @property (strong) IBOutlet NSWindow *helpWindow; -@property (assign) IBOutlet NSTextView *helpTextView; +@property (assign) IBOutlet PDFView *helpView; -@property (strong) IBOutlet NSUserDefaultsController *preferencesController; -@property (strong) IBOutlet TNXcodeCapp *xcc; +@property (strong) IBOutlet NSUserDefaultsController *preferencesController; +@property (strong) IBOutlet XcodeCapp *xcc; + (AppController *)sharedAppController; -- (IBAction)listenToProject:(id)aSender; +- (IBAction)loadProject:(id)aSender; - (IBAction)openHelp:(id)aSender; - (IBAction)openAbout:(id)aSender; +- (void)loadProjectAtPath:(NSString *)path; + @end diff --git a/Tools/XcodeCapp/XcodeCapp/AppController.m b/Tools/XcodeCapp/XcodeCapp/AppController.m index b33ae4b89..94f4ecbef 100644 --- a/Tools/XcodeCapp/XcodeCapp/AppController.m +++ b/Tools/XcodeCapp/XcodeCapp/AppController.m @@ -20,24 +20,22 @@ #import #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 diff --git a/Tools/XcodeCapp/XcodeCapp/DDLogLevel.h b/Tools/XcodeCapp/XcodeCapp/DDLogLevel.h new file mode 100644 index 000000000..e1c80eb82 --- /dev/null +++ b/Tools/XcodeCapp/XcodeCapp/DDLogLevel.h @@ -0,0 +1,17 @@ +// +// DDLogLevel.h +// XcodeCapp +// +// Created by Aparajita on 4/29/13. +// Copyright (c) 2013 Cappuccino Project. All rights reserved. +// + +#import + +extern int ddLogLevel; + +@interface DDLogLevel : NSObject + ++ (void)setLogLevel:(int)logLevel; + +@end diff --git a/Tools/XcodeCapp/XcodeCapp/DDLogLevel.m b/Tools/XcodeCapp/XcodeCapp/DDLogLevel.m new file mode 100644 index 000000000..a0fb829b1 --- /dev/null +++ b/Tools/XcodeCapp/XcodeCapp/DDLogLevel.m @@ -0,0 +1,27 @@ +// +// DDLog.m +// XcodeCapp +// +// Created by Aparajita on 4/29/13. +// Copyright (c) 2013 Cappuccino Project. All rights reserved. +// + +#import "DDLogLevel.h" + + +int ddLogLevel = LOG_LEVEL_OFF; + + +@implementation DDLogLevel + ++ (int)ddLogLevel +{ + return ddLogLevel; +} + ++ (void)setLogLevel:(int)logLevel +{ + ddLogLevel = logLevel; +} + +@end diff --git a/Tools/XcodeCapp/XcodeCapp/FSEventCallback.h b/Tools/XcodeCapp/XcodeCapp/FSEventCallback.h deleted file mode 100644 index 7f845f245..000000000 --- a/Tools/XcodeCapp/XcodeCapp/FSEventCallback.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * This file is a part of program XcodeCapp - * Copyright (C) 2011 Antoine Mercadal () - * - * 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 . - */ - -#ifndef xcodecapp_cocoa_FSEventCallback_h -#define xcodecapp_cocoa_FSEventCallback_h - -#import "TNXcodeCapp.h" -#import - -#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_6 -# define kFSEventStreamCreateFlagFileEvents 0x00000010 -# define kFSEventStreamEventFlagItemIsFile 0x00010000 -# define kFSEventStreamEventFlagItemRemoved 0x00000200 -# define kFSEventStreamEventFlagItemCreated 0x00000200 -# define kFSEventStreamEventFlagItemModified 0x00001000 -# define kFSEventStreamEventFlagItemInodeMetaMod 0x00000400 -# define kFSEventStreamEventFlagItemRenamed 0x00000800 -# define kFSEventStreamEventFlagItemFinderInfoMod 0x00002000 -# define kFSEventStreamEventFlagItemChangeOwner 0x00004000 -# define kFSEventStreamEventFlagItemXattrMod 0x00008000 -#endif - -void fsevents_callback(ConstFSEventStreamRef, void*, size_t, void*, const FSEventStreamEventFlags*, const FSEventStreamEventId*); - -#endif diff --git a/Tools/XcodeCapp/XcodeCapp/FSEventCallback.m b/Tools/XcodeCapp/XcodeCapp/FSEventCallback.m deleted file mode 100644 index 02cd2c83d..000000000 --- a/Tools/XcodeCapp/XcodeCapp/FSEventCallback.m +++ /dev/null @@ -1,115 +0,0 @@ -/* - * This file is a part of program XcodeCapp - * Copyright (C) 2011 Antoine Mercadal () - * - * 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 . - */ - -#import "AppController.h" -#import "FSEventCallback.h" -#import "macros.h" - -void fsevents_callback(ConstFSEventStreamRef streamRef, - void *userData, - size_t numEvents, - void *eventPaths, - const FSEventStreamEventFlags eventFlags[], - const FSEventStreamEventId eventIds[]) -{ - TNXcodeCapp *xcc = (__bridge TNXcodeCapp *)userData; - NSArray *paths = (__bridge NSArray *)eventPaths; - BOOL usingFileBasedListening = [xcc supportsFileBasedListening]; - - for (size_t i = 0; i < numEvents; ++i) - { - [xcc updateLastEventId:eventIds[i]]; - - FSEventStreamEventFlags flags = eventFlags[i]; - NSString *path = [[paths objectAtIndex:i] stringByStandardizingPath]; - - if (usingFileBasedListening) - { - if ([xcc pathMatchesIgnoredPaths:path]) - continue; - - if ((flags & kFSEventStreamEventFlagItemIsFile) && - !([xcc isXibFile:path] || [xcc isObjjFile:path] || [xcc isXCCIgnoreFile:path])) - { - continue; - } - - // Events are not so reliable. For example, moving a folder to the trash is not - // a deletion. In order to simplify the code, we simply tidy up the project when we receive - // an event. - [xcc tidyShadowedFiles]; - - BOOL isDirectory = NO; - BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]; - - if (isDirectory) - continue; - - if (!exists) - { - DLog(@"File removed: %@", path); - [xcc handleFileRemovalAtPath:path]; - } - else - { - DLog(@"File modified/added: %@", path); - [xcc handleFileModificationAtPath:path notify:YES]; - } - } - else - { - // We should drop support for Snow Leopard soon. - - BOOL isDirectory = NO; - [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]; - - // If for some reasons the path is not a directory, - // we don't want to deal with it in this mode. - if (!isDirectory) - continue; - - [xcc tidyShadowedFiles]; - - NSFileManager *fm = [NSFileManager defaultManager]; - NSArray *subpaths = [fm contentsOfDirectoryAtPath:path error:NULL]; - - for (NSString *subpath in subpaths) - { - NSString *fullPath = [path stringByAppendingPathComponent:subpath]; - - if ([xcc pathMatchesIgnoredPaths:fullPath] || - !([xcc isXibFile:fullPath] || [xcc isObjjFile:fullPath] || [xcc isXCCIgnoreFile:fullPath])) - { - continue; - } - - NSDate *lastModifiedDate = [xcc lastModificationDateForPath:fullPath]; - NSDictionary *fileAttributes = [fm attributesOfItemAtPath:fullPath error:nil]; - NSDate *fileModDate = [fileAttributes objectForKey:NSFileModificationDate]; - - if ([fileModDate compare:lastModifiedDate] == NSOrderedDescending) - { - [xcc updateLastModificationDate:fileModDate forPath:fullPath]; - [xcc handleFileModificationAtPath:fullPath notify:YES]; - } - } - } - } - - [xcc updateUserDefaultsWithLastEventId]; -} diff --git a/Tools/XcodeCapp/XcodeCapp/FindSourceFilesOperation.h b/Tools/XcodeCapp/XcodeCapp/FindSourceFilesOperation.h new file mode 100644 index 000000000..ce4d5eb04 --- /dev/null +++ b/Tools/XcodeCapp/XcodeCapp/FindSourceFilesOperation.h @@ -0,0 +1,21 @@ +// +// FindSourceFilesOperation.h +// XcodeCapp +// +// Created by Aparajita on 4/27/13. +// Copyright (c) 2013 Cappuccino Project. All rights reserved. +// + +#import + +@class XcodeCapp; + + +extern NSString * const XCCNeedSourceToProjectPathMappingNotification; + + +@interface FindSourceFilesOperation : NSOperation + +- (id)initWithXCC:(XcodeCapp *)xcc projectId:(NSNumber *)projectId path:(NSString *)path; + +@end diff --git a/Tools/XcodeCapp/XcodeCapp/FindSourceFilesOperation.m b/Tools/XcodeCapp/XcodeCapp/FindSourceFilesOperation.m new file mode 100644 index 000000000..6bdb08794 --- /dev/null +++ b/Tools/XcodeCapp/XcodeCapp/FindSourceFilesOperation.m @@ -0,0 +1,165 @@ +// +// FindSourceFilesOperation.m +// XcodeCapp +// +// Created by Aparajita on 4/27/13. +// Copyright (c) 2013 Cappuccino Project. All rights reserved. +// + +#import "FindSourceFilesOperation.h" +#import "ProcessSourceOperation.h" +#import "XcodeCapp.h" + +NSString * const XCCNeedSourceToProjectPathMappingNotification = @"XCCNeedSourceToProjectPathMappingNotification"; + + +@interface FindSourceFilesOperation () + +@property XcodeCapp *xcc; +@property NSNumber *projectId; +@property NSString *projectPathToSearch; +@property NSString *projectPath; + +@end + + +@implementation FindSourceFilesOperation + +- (id)initWithXCC:(XcodeCapp *)xcc projectId:(NSNumber *)projectId path:(NSString *)path +{ + self = [super init]; + + if (self) + { + self.xcc = xcc; + self.projectId = projectId; + self.projectPathToSearch = path; + self.projectPath = xcc.projectPath; + } + + return self; +} + +- (void)main +{ + [self findSourceFilesAtProjectPath:self.projectPathToSearch]; +} + +- (void)findSourceFilesAtProjectPath:(NSString *)aProjectPath +{ + if (self.isCancelled) + return; + + DDLogVerbose(@"-->findSourceFiles: %@", aProjectPath); + + NSError *error = NULL; + NSString *projectPath = [self.projectPath stringByAppendingPathComponent:aProjectPath]; + NSFileManager *fm = [NSFileManager defaultManager]; + + NSArray *urls = [fm contentsOfDirectoryAtURL:[NSURL fileURLWithPath:projectPath.stringByResolvingSymlinksInPath] + includingPropertiesForKeys:@[NSURLIsDirectoryKey, NSURLIsSymbolicLinkKey] + options:NSDirectoryEnumerationSkipsHiddenFiles | NSDirectoryEnumerationSkipsPackageDescendants | NSDirectoryEnumerationSkipsSubdirectoryDescendants + error:&error]; + + if (!urls) + return; + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + + for (NSURL *url in urls) + { + if (self.isCancelled) + return; + + NSString *filename = url.lastPathComponent; + + NSString *projectRelativePath = [aProjectPath stringByAppendingPathComponent:filename]; + NSString *realPath = url.path; + NSURL *resolvedURL = url; + + NSNumber *isDirectory, *isSymlink; + [url getResourceValue:&isSymlink forKey:NSURLIsSymbolicLinkKey error:nil]; + + if (isSymlink.boolValue == YES) + { + resolvedURL = [url URLByResolvingSymlinksInPath]; + + if ([resolvedURL checkResourceIsReachableAndReturnError:nil]) + { + filename = resolvedURL.lastPathComponent; + realPath = resolvedURL.path; + } + else + continue; + } + + [resolvedURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; + + if (isDirectory.boolValue == YES) + { + if ([self.xcc shouldIgnoreDirectoryNamed:filename]) + continue; + + // If the resolved path is not within the project directory and is not ignored, add a mapping to it + // so we can map the resolved path back to the project directory later. + if (isSymlink.boolValue == YES) + { + NSString *fullProjectPath = [self.projectPath stringByAppendingPathComponent:projectRelativePath]; + + if (![realPath hasPrefix:fullProjectPath] && ![self.xcc pathMatchesIgnoredPaths:fullProjectPath]) + { + NSDictionary *info = + @{ + @"projectId":self.projectId, + @"sourcePath":realPath, + @"projectPath":fullProjectPath + }; + + if (self.isCancelled) + return; + + [center postNotificationName:XCCNeedSourceToProjectPathMappingNotification object:self userInfo:info]; + } + } + + [self findSourceFilesAtProjectPath:projectRelativePath]; + continue; + } + + if (self.isCancelled) + return; + + if ([self.xcc pathMatchesIgnoredPaths:realPath]) + continue; + + NSString *projectSourcePath = [self.projectPath stringByAppendingPathComponent:projectRelativePath]; + + if ([self.xcc isObjjFile:filename] || [self.xcc isXibFile:filename]) + { + NSString *processedPath; + + if ([self.xcc isObjjFile:filename]) + processedPath = [[self.xcc shadowBasePathForProjectSourcePath:projectSourcePath] stringByAppendingPathExtension:@"h"]; + else + processedPath = [projectSourcePath.stringByDeletingPathExtension stringByAppendingPathExtension:@"cib"]; + + if (![fm fileExistsAtPath:processedPath]) + [self createProcessingOperationForProjectSourcePath:projectSourcePath]; + } + } + + DDLogVerbose(@"<--findSourceFiles: %@", aProjectPath); +} + +- (void)createProcessingOperationForProjectSourcePath:(NSString *)projectSourcePath +{ + if (self.isCancelled) + return; + + ProcessSourceOperation *op = [[ProcessSourceOperation alloc] initWithXCC:self.xcc + projectId:self.projectId + sourcePath:projectSourcePath]; + [[NSOperationQueue currentQueue] addOperation:op]; +} + +@end diff --git a/Tools/XcodeCapp/XcodeCapp/Info.plist b/Tools/XcodeCapp/XcodeCapp/Info.plist index 780a34d48..f0b4db35a 100644 --- a/Tools/XcodeCapp/XcodeCapp/Info.plist +++ b/Tools/XcodeCapp/XcodeCapp/Info.plist @@ -30,5 +30,7 @@ MainMenu NSPrincipalClass NSApplication + XCCCompatibilityVersion + 3 diff --git a/Tools/XcodeCapp/XcodeCapp/Notifications.h b/Tools/XcodeCapp/XcodeCapp/Notifications.h new file mode 100644 index 000000000..b5478399a --- /dev/null +++ b/Tools/XcodeCapp/XcodeCapp/Notifications.h @@ -0,0 +1,19 @@ +// +// Notifications.h +// XcodeCapp +// +// Created by Aparajita on 4/27/13. +// Copyright (c) 2013 Cappuccino Project. All rights reserved. +// + +#ifndef XcodeCapp_Notifications_h +#define XcodeCapp_Notifications_h + +extern NSString * const XCCProjectDidFinishLoadingNotification; +extern NSString * const XCCBatchDidStartNotification; +extern NSString * const XCCBatchDidEndNotification; +extern NSString * const XCCConversionDidStartNotification; +extern NSString * const XCCConversionDidEndNotification; +extern NSString * const XCCConversionDidGenerateErrorNotification; + +#endif diff --git a/Tools/XcodeCapp/XcodeCapp/Notifications.m b/Tools/XcodeCapp/XcodeCapp/Notifications.m new file mode 100644 index 000000000..b8df14b4f --- /dev/null +++ b/Tools/XcodeCapp/XcodeCapp/Notifications.m @@ -0,0 +1,17 @@ +// +// Notifications.m +// XcodeCapp +// +// Created by Aparajita on 4/27/13. +// +// + +#include "Notifications.h" + + +NSString * const XCCProjectDidFinishLoadingNotification = @"XCCProjectDidFinishLoadingNotification"; +NSString * const XCCBatchDidStartNotification = @"XCCBatchDidStartNotification"; +NSString * const XCCBatchDidEndNotification = @"XCCBatchDidEndNotification"; +NSString * const XCCConversionDidStartNotification = @"XCCConversionDidStartNotification"; +NSString * const XCCConversionDidEndNotification = @"XCCConversionDidStopNotification"; +NSString * const XCCConversionDidGenerateErrorNotification = @"XCCConversionDidGenerateErrorNotification"; \ No newline at end of file diff --git a/Tools/XcodeCapp/XcodeCapp/ProcessSourceOperation.h b/Tools/XcodeCapp/XcodeCapp/ProcessSourceOperation.h new file mode 100644 index 000000000..e8254ddd4 --- /dev/null +++ b/Tools/XcodeCapp/XcodeCapp/ProcessSourceOperation.h @@ -0,0 +1,19 @@ +// +// ProcessSourceOperation.h +// XcodeCapp +// +// Created by Aparajita on 4/27/13. +// Copyright (c) 2013 Cappuccino Project. All rights reserved. +// + +#import + +@class XcodeCapp; + + +@interface ProcessSourceOperation : NSOperation + +// sourcePath should be a path within the project (no resolved symlinks) +- (id)initWithXCC:(XcodeCapp *)xcc projectId:(NSNumber *)projectId sourcePath:(NSString *)sourcePath; + +@end diff --git a/Tools/XcodeCapp/XcodeCapp/ProcessSourceOperation.m b/Tools/XcodeCapp/XcodeCapp/ProcessSourceOperation.m new file mode 100644 index 000000000..df268b11b --- /dev/null +++ b/Tools/XcodeCapp/XcodeCapp/ProcessSourceOperation.m @@ -0,0 +1,184 @@ +// +// ProcessSourceOperation.m +// XcodeCapp +// +// Created by Aparajita on 4/27/13. +// Copyright (c) 2013 Cappuccino Project. All rights reserved. +// + +#import "ProcessSourceOperation.h" +#import "Notifications.h" +#import "XcodeCapp.h" + + +@interface ProcessSourceOperation () + +@property XcodeCapp *xcc; +@property NSNumber *projectId; +@property NSString *sourcePath; +@property NSString *projectPath; + +@end + + +@implementation ProcessSourceOperation + +- (id)initWithXCC:(XcodeCapp *)xcc projectId:(NSNumber *)projectId sourcePath:(NSString *)sourcePath +{ + self = [super init]; + + if (self) + { + self.xcc = xcc; + self.projectId = projectId; + self.sourcePath = sourcePath; + self.projectPath = xcc.projectPath; + } + + return self; +} + +- (void)main +{ + if (self.isCancelled) + return; + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + + NSDictionary *info = @{ @"projectId":self.projectId, @"path":self.sourcePath }; + [center postNotificationName:XCCConversionDidStartNotification object:self userInfo:info]; + + DDLogVerbose(@"Conversion started: %@", self.sourcePath); + + NSString *launchPath = nil; + NSArray *arguments = nil; + NSString *response = nil; + NSString *projectRelativePath = [self.sourcePath substringFromIndex:self.projectPath.length + 1]; + NSString *notificationTitle = nil; + NSString *notificationMessage = projectRelativePath.lastPathComponent; + + if ([self.xcc isXibFile:self.sourcePath]) + { + launchPath = self.xcc.executablePaths[@"nib2cib"]; + arguments = @[ + @"--no-colors", + self.sourcePath + ]; + + notificationTitle = @"Xib converted"; + } + else if ([self.xcc isObjjFile:self.sourcePath]) + { + launchPath = self.xcc.executablePaths[@"objj"]; + arguments = @[ + self.xcc.parserPath, + self.projectPath, + self.sourcePath + ]; + + notificationTitle = @"Objective-J source processed"; + } + else if ([self.xcc isXCCIgnoreFile:self.sourcePath]) + { + if (self.isCancelled) + return; + + [self.xcc performSelectorOnMainThread:@selector(computeIgnoredPaths) withObject:nil waitUntilDone:NO]; + + notificationTitle = @"Parsed .xcodecapp-ignore"; + notificationMessage = @"Ignored paths updated"; + } + + // Run the task and get the response if needed + NSInteger status = 0; + + if (arguments) + { + if (self.isCancelled) + return; + + DDLogVerbose(@"Running processing task: %@", launchPath); + + NSDictionary *taskResult = [self.xcc runTaskWithLaunchPath:launchPath + arguments:arguments + returnType:kTaskReturnTypeStdError]; + + status = [taskResult[@"status"] intValue]; + response = taskResult[@"response"]; + + DDLogInfo(@"Processed %@: [%ld, %@]", self.sourcePath, status, status ? response : @""); + + if (self.isCancelled) + return; + + if (status != 0) + { + if (response.length == 0) + response = @"An unspecified error occurred"; + + if ([self.xcc isXibFile:self.sourcePath]) + { + notificationTitle = @"Error converting xib"; + NSString *message = [NSString stringWithFormat:@"%@\n%@", self.sourcePath.lastPathComponent, response]; + + NSDictionary *info = + @{ + @"projectId":self.projectId, + @"message":message, + @"path":self.sourcePath, + @"status":taskResult[@"status"] + }; + + if (self.isCancelled) + return; + + [center postNotificationName:XCCConversionDidGenerateErrorNotification object:self userInfo:info]; + } + else + { + notificationTitle = [(status == XCCStatusCodeError ? @"Error" : @"Warning") stringByAppendingString:@" parsing Objective-J source"]; + + NSArray *errors = [response propertyList]; + + for (NSDictionary *error in errors) + { + NSMutableDictionary *info = [error mutableCopy]; + info[@"projectId"] = self.projectId; + info[@"message"] = [NSString stringWithFormat:@"%@, line %d\n%@", [error[@"path"] lastPathComponent], [error[@"line"] intValue], error[@"message"]]; + info[@"status"] = taskResult[@"status"]; + + if (self.isCancelled) + return; + + [center postNotificationName:XCCConversionDidGenerateErrorNotification object:self userInfo:info]; + } + } + + [self notifyUserWithTitle:notificationTitle message:notificationMessage]; + } + else if (!self.xcc.isLoadingProject) + { + [self notifyUserWithTitle:notificationTitle message:notificationMessage]; + } + } + + if (!self.isCancelled) + { + DDLogVerbose(@"Conversion ended: %@", self.sourcePath); + + [center postNotificationName:XCCConversionDidEndNotification object:self userInfo:@{ @"projectId":self.projectId, @"path":self.sourcePath }]; + } +} + +- (void)notifyUserWithTitle:(NSString *)title message:(NSString *)message +{ + NSDictionary *info = @{ @"projectId":self.projectId, @"title":title, @"message":message }; + + if (self.isCancelled) + return; + + // nib2cib can take a while to run, show a message while the conversion is happening + [self.xcc wantUserNotificationWithInfo:info]; +} + +@end diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/XcodeCapp help.pages b/Tools/XcodeCapp/XcodeCapp/Resources/XcodeCapp help.pages new file mode 100644 index 000000000..8164675a1 Binary files /dev/null and b/Tools/XcodeCapp/XcodeCapp/Resources/XcodeCapp help.pages differ diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/TXT.rtf b/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/TXT.rtf deleted file mode 100644 index b7739f2bc..000000000 --- a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/TXT.rtf +++ /dev/null @@ -1,390 +0,0 @@ -{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 -{\fonttbl\f0\fnil\fcharset0 LucidaGrande;\f1\fswiss\fcharset0 Helvetica;\f2\fmodern\fcharset0 Courier; -} -{\colortbl;\red255\green255\blue255;} -{\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{square\}}{\leveltext\leveltemplateid1\'01\uc0\u9642 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid1} -{\list\listtemplateid2\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}.}{\leveltext\leveltemplateid101\'02\'00.;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid2} -{\list\listtemplateid3\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}.}{\leveltext\leveltemplateid201\'02\'00.;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid3} -{\list\listtemplateid4\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{square\}}{\leveltext\leveltemplateid301\'01\uc0\u9642 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid4} -{\list\listtemplateid5\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{square\}}{\leveltext\leveltemplateid401\'01\uc0\u9642 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid5} -{\list\listtemplateid6\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{square\}}{\leveltext\leveltemplateid501\'01\uc0\u9642 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid6} -{\list\listtemplateid7\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{square\}}{\leveltext\leveltemplateid601\'01\uc0\u9642 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid7} -{\list\listtemplateid8\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{square\}}{\leveltext\leveltemplateid701\'01\uc0\u9642 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid8} -{\list\listtemplateid9\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}.}{\leveltext\leveltemplateid801\'02\'00.;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid9} -{\list\listtemplateid10\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{square\}}{\leveltext\leveltemplateid901\'01\uc0\u9642 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid10} -{\list\listtemplateid11\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}.}{\leveltext\leveltemplateid1001\'02\'00.;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid11} -{\list\listtemplateid12\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}.}{\leveltext\leveltemplateid1101\'02\'00.;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid12}} -{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}{\listoverride\listid2\listoverridecount0\ls2}{\listoverride\listid3\listoverridecount0\ls3}{\listoverride\listid4\listoverridecount0\ls4}{\listoverride\listid5\listoverridecount0\ls5}{\listoverride\listid6\listoverridecount0\ls6}{\listoverride\listid7\listoverridecount0\ls7}{\listoverride\listid8\listoverridecount0\ls8}{\listoverride\listid9\listoverridecount0\ls9}{\listoverride\listid10\listoverridecount0\ls10}{\listoverride\listid11\listoverridecount0\ls11}{\listoverride\listid12\listoverridecount0\ls12}} -\margl1440\margr1440\vieww16380\viewh15560\viewkind0 -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f0\b\fs36 \cf0 XcodeCapp Help -\b0 \ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\b\fs28 \cf0 What is XcodeCapp?\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\b0\fs36 \cf0 \ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\fs24 \cf0 One of Cappuccino\'92s greatest features is its ability to use Xcode 4 to create the user interface for your web applications. Xcode creates .xib files which then must be converted by the command line utility -\b nib2cib -\b0 to .cib files for use with Cappuccino. But beginning with Xcode 4, there is no way to directly create outlets and actions without editing Objective-C header files.\ -\ -XcodeCapp acts as a bridge between Xcode and Cappuccino. It performs two main functions:\ -\ -\pard\tx220\tx720\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\li720\fi-720\pardirnatural -\ls1\ilvl0\cf0 {\listtext \uc0\u9642 }Creates an Xcode project where you can edit your Cappuccino project\'92s xibs.\ -{\listtext \uc0\u9642 }Reads your source files and automatically creates outlets and actions in the xib file.\ -{\listtext \uc0\u9642 }Automatically converts xib files to cib files when the .xib file is modified.\ -{\listtext \uc0\u9642 }Listens for changes to source files and xibs and updates outlets and actions in the xib file accordingly.\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural -\cf0 \ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\b\fs28 \cf0 Using XcodeCapp -\b0 \ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\fs24 \cf0 \ -XcodeCapp is very easy to use and requires very little user interaction. When you build Cappuccino with jake, it will create a symlink to the XcodeCapp application in your Applications folder. Launch XcodeCapp from there and the XcodeCapp icon will appear in your menu bar. Clicking on the icon will display the XcodeCapp menu:\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f1 \cf0 {{\NeXTGraphic menu.png \width5000 \height5160 -}}\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f0 \cf0 \ -\ -When the status icon is light gray, XcodeCapp does not have a project open.\ -\ - -\b \ -Opening a Project\ - -\b0 \ -First you have to tell XcodeCapp what Cappuccino project to open.\ -\ -\pard\tx220\tx720\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\li720\fi-720\pardirnatural -\ls2\ilvl0\cf0 {\listtext 1. }Select \'93Open Project\'85\'94 from the XcodeCapp menu. A folder chooser dialog will appear.\ -{\listtext 2. }Navigate to the root folder of your project \'97 the one that contains index-debug.html and Jakefile.\ -{\listtext 3. }Click Open.\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural -\cf0 \ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural -\cf0 Once you open a project, XcodeCapp does the following:\ -\ -\pard\tx220\tx720\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\li720\fi-720\pardirnatural -\ls3\ilvl0\cf0 {\listtext 1. }Creates an Xcode project with the name of the project directory, if no such project already exists.\ -{\listtext 2. }Parses source code in the project directory and all non-Cappuccino subdirectories (such as Build, Frameworks, etc.).\ -{\listtext 3. }Parses source code in non-Cappuccino frameworks.\ -{\listtext 4. }If no cib file exists for a given xib file, converts the xib using nib2cib.\ -\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural -\cf0 \ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural -\cf0 When XcodeCapp is processing files, the status icon changes to light blue.\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f1 \cf0 {{\NeXTGraphic loading-project.png \width6560 \height1960 -}}\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f0 \cf0 \ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural -\cf0 \ -During source parsing, XcodeCapp creates an invisible folder called \'93.XcodeSupport\'94 in the root project directory. This folder contains files created by XcodeCapp that act as the bridge between Cappuccino and Xcode, so you should never directly modify those files. You will probably want to ignore this folder in your IDE and source code management system as well.\ -\ -After XcodeCapp has opened a project, it will listen for changes to the following files anywhere in the project:\ -\ -\pard\tx220\tx720\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\li720\fi-720\pardirnatural -\ls4\ilvl0\cf0 {\listtext \uc0\u9642 }*.xib \'96 Xcode 4 interface builder files\ -{\listtext \uc0\u9642 }*.j - Objective-J source\ -{\listtext \uc0\u9642 }.xcodecapp-ignore - Specifies files XcodeCapp should ignore. See \'93Ignoring Files and Folders\'94 below.\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural -\cf0 \ -When XcodeCapp has a project open and is listening for changes, the status icon changes to dark gray.\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f1 \cf0 {{\NeXTGraphic active.png \width660 \height420 -}}\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f0 \cf0 \ -\ -The most recently opened project is inserted at the top of the Open Recent submenu in the XcodeCapp menu. By default, the recent project list is limited to 20 projects. You can change that number in the Preferences window. See \'93Preferences\'94 below for more information.\ -\ -If you quit XcodeCapp while a project is open, by default when you reopen XcodeCapp it will reopen the most recently opened project. You can change this behavior in the Preferences window. See \'93Preferences\'94 below for more information.\ -\ -\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural - -\b \cf0 Notifications\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural - -\b0 \cf0 \ -XcodeCapp shows notifications whenever it loads a project or processes files. If you are using Mac OS X 10.8+, notifications will appear in Notification Center. Otherwise they will appear according to Growl\'92s preferences if Growl is installed, or in a simple notification window if Growl is not installed. To turn notifications off, use the Notifications control panel (OS X 10.8+) or Growl preferences.\ -\ -Note that notifications remain on screen for a minimum amount of time, and thus may lag behind the actual processing of the files.\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural -\cf0 \ -\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural - -\b \cf0 Handling Errors\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural - -\b0 \cf0 \ -If an error or warning occurs during the parsing of an Objective-J file or the conversion of a xib, XcodeCapp notifies you in two ways:\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural -\cf0 \ -\pard\tx220\tx720\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\li720\fi-720\pardirnatural -\ls5\ilvl0\cf0 {\listtext \uc0\u9642 }If an error occurs, the status icon becomes red.\ -\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\sb120\pardirnatural -\cf0 -\f1 {{\NeXTGraphic error.png \width620 \height420 -}} -\f0 \ -\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\sl300\slmult1\pardirnatural -\cf0 \ -\pard\tx220\tx720\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\li720\fi-720\sl300\slmult1\pardirnatural -\ls6\ilvl0\cf0 {\listtext \uc0\u9642 }If an error or warning occurs, by default XcodeCapp will open the Errors & Warnings panel and display information about the error or warning.\ -\pard\tx560\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\sb120\pardirnatural -\cf0 -\f1 {{\NeXTGraphic errors-panel.png \width11780 \height5900 -}} -\f0 \ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural -\cf0 \ -Selecting an error in the list and clicking Open or double-clicking on an error opens the offending file in the preferred editor for that file and closes the Errors panel. In the case of .j files, XcodeCapp will attempt to open the file directly to the offending line in the following editors:\ -\ -\pard\tx220\tx720\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\li720\fi-720\pardirnatural -\ls7\ilvl0\cf0 {\listtext \uc0\u9642 }Sublime Text\ -{\listtext \uc0\u9642 }TextMate\ -{\listtext \uc0\u9642 }TextWrangler\ -{\listtext \uc0\u9642 }BBEdit\ -{\listtext \uc0\u9642 }Chocolat\ -{\listtext \uc0\u9642 }MacVIM\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural -\cf0 \ -The error list is cleared when a new project is opened. Also, whenever a file is modified, any existing errors for that file are removed. So the Errors & Warnings panel always represents the current state of your files.\ -\ -If you do not wish the Errors panel to open automatically when errors or warnings occur, you can change that behavior in the Preferences window. See \'93Preferences\'94 below for more information.\ -\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural - -\b \cf0 \ -Declaring Outlets and Actions\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural - -\b0 \cf0 \ -The most important function XcodeCapp performs is to create outlets and actions for use with Xcode. You create outlets by prefixing them in your source files with -\b @outlet -\b0 or -\b IBOutlet -\b0 . For example, in the class below, there are four outlets defined:\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f1 \cf0 {{\NeXTGraphic outlets.png \width10720 \height4820 -}}\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f0 \cf0 \ -\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural -\cf0 Similarly, you declare actions by declaring the return type of the method to be -\b @action -\b0 or -\b IBAction -\b0 . For example, the following method can serve as an action for a control:\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f1 \cf0 {{\NeXTGraphic actions.png \width10800 \height1580 -}}\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f0 \cf0 \ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural -\cf0 \ -\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural - -\b \cf0 Editing Xibs\ -\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural - -\b0 \cf0 When you open a project, XcodeCapp creates an Xcode 4 project with which you to edit your project\'92s xibs. To open the Xcode project, click on the XcodeCapp menu and select \'93Open Project in Xcode\'94.\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural -\cf0 \ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f1 \cf0 {{\NeXTGraphic open-project-in-xcode.png \width5900 \height5060 -}}\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f0 \cf0 \ -\ -Once the Xcode project is open, you can edit your xibs with the interface builder. The Xcode project contains three folders by default:\ -\ -\pard\tx220\tx720\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\li720\fi-720\pardirnatural -\ls8\ilvl0\cf0 {\listtext \uc0\u9642 }Resources \'96 Contains the files in your project\'92s Resources directory.\ -{\listtext \uc0\u9642 }Classes \'96 Contains the Objective-C classes generated by XcodeCapp. You should not edit or remove these.\ -{\listtext \uc0\u9642 }Sources \'96 Contains the source files found by XcodeCapp. You may edit these files in Xcode if you wish.\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural -\cf0 \ -All of the outlets and actions you declared in your source will be available in interface builder for connection to views. For example, the InfoPanelController shown above would have these outlets in interface builder:\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural -\cf0 \ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f1 \cf0 {{\NeXTGraphic outlets-xcode.png \width9760 \height5960 -}}\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f0 \cf0 \ -\ -To add more outlets or actions to the xib, you must edit the Cappuccino source and wait briefly while XcodeCapp processes the changes; do not attempt to add an outlet or action in Xcode.\ -\ -\ - -\b Working with User Frameworks\ -\ - -\b0 When a project is opened, XcodeCapp looks for user (non-Cappuccino) frameworks in your project\'92s Frameworks/Debug directory. Any debug user frameworks in source form will be processed by XcodeCapp. This allows you to declare outlets and actions in frameworks and use them within Xcode. In addition, if a framework contains a Resources directory, a \'93 Resources\'94 folder referencing that directory is created in the Xcode project. This allows you to reference framework resources within Xcode.\ -\ -If you want your user frameworks to execute in compiled form, but you still want XcodeCapp to have access to the framework\'92s source in order to parse outlets and actions, do the following:\ -\ -\pard\tx220\tx720\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\li720\fi-720\pardirnatural -\ls9\ilvl0\cf0 {\listtext 1. }Copy or symlink to the compiled framework as usual within your project\'92s Frameworks directory.\ -{\listtext 2. }Create a directory called \'93Source\'94 within your project\'92s Frameworks directory.\ -{\listtext 3. }Within the Source directory, copy or symlink to the framework source directory.\ -{\listtext 4. }XcodeCapp will recursively search the Frameworks/Source directory when a project is opened.\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural -\cf0 \ -For example, here is the structure of a project\'92s Frameworks directory:\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f1 \cf0 {{\NeXTGraphic frameworks.png \width6320 \height7060 -}}\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f0 \cf0 \ -\ -Note the following:\ -\ -\pard\tx220\tx720\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\li720\fi-720\pardirnatural -\ls10\ilvl0\cf0 {\listtext \uc0\u9642 }Debug/JQueryFileUpload is a symlink to the compiled framework. This is what will be executed when the app is run in debug mode.\ -{\listtext \uc0\u9642 }The Source directory contains a symlink to the JQueryFileUpload source. This will be parsed by XcodeCapp.\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural -\cf0 \ -If you need to use image resources located in a framework \'97 for example in an NSButton or NSImageView \'97 you have two choices:\ -\ -\pard\tx220\tx720\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\li720\fi-720\pardirnatural -\ls11\ilvl0\cf0 {\listtext 1. }Use the base image name alone, for example \'93cancel\'94, which you wish to represent the image \'93cancel.png\'94 in the framework \'93JQueryFileUpload\'94.\ -{\listtext 2. }Use a full name@framework specification, for example \'93cancel@ JQueryFileUpload\'94 (without the quotes).\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural -\cf0 \ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural -\cf0 In the first form, during conversion, nib2cib will first search the app\'92s Resources directory for an image with the given name, then it will search all of the Resources directories of the debug frameworks, followed by the release frameworks. The first match wins, and if the image was found in a framework, the framework\'92s bundle identifier is recorded so the image can be loaded from the correct bundle at runtime.\ -\ -Since there might be multiple images in the search path that have the same name, if you want to ensure you are using an image from a specific framework, use the second form. During conversion, nib2cib will check the named framework for the given image, and if it is found the framework\'92s bundle identifier is recorded so the image can be loaded from the correct bundle at runtime.\ -\ -\ - -\b Ignoring Files and Folders\ -\ - -\b0 There may be source which you do not want XcodeCapp to process. To specify directories or files which you want XcodeCapp to ignore, do the following:\ -\ -\pard\tx220\tx720\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\li720\fi-720\pardirnatural -\ls12\ilvl0\cf0 {\listtext 1. }Create a text file in the root of the project directory named \'93.xcodecapp-ignore\'94.\ -{\listtext 2. }Enter one line for each pattern you would like to ignore, where a pattern matches the -\b absolute -\b0 (full) path to the file or directory.\ -{\listtext 3. }Save the file. Whenever .xcodecapp-ignore is modified, XcodeCapp will parse it to rebuild the ignore list.\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural -\cf0 \ -Patterns may use \'93*\'94 as a wildcard to match zero or more characters (it is -\b not -\b0 a shell glob) and are matched against the -\b absolute -\b0 path of the file or directory, so in most cases your patterns should begin with \'93*\'94. To ignore a directory, add \'93/\'94 at the end of the directory name. For example, \'93*/server/\'94 will ignore that directory and any files within it.\ -\ -For example, to ignore the \'93Modules\'94 directory and the xib file \'93foo.xib\'94 in your project, enter these lines in .xcodecapp-ignore:\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f2 \cf0 */Modules/\ -*/foo.xib\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f0 \cf0 \ -Sometimes you may want to ignore almost all of the files or directories within a given directory. In such cases you can use an include pattern. An include pattern has the same syntax as the normal exclude patterns, but begins with \'93!\'94. Since ignore patterns are parsed in the order they appear in .xcodecapp-ignore, an include pattern should come after the exclude pattern it overrides. For example, if you wanted to exclude everything in the Modules directory, except for the Classes directory and any xib files, you would use the following patterns:\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f2 \cf0 */Modules/\ -!*/Modules/Classes/\ -!*/Modules/*.xib -\f0 \ -\ -Note that in addition to whatever patterns you specify (if any) in .xcodecapp-ignore, XcodeCapp -\b always -\b0 parses the following patterns -\b before -\b0 your patterns:\ -\ - -\f2 */Frameworks/\ -!*/Frameworks/Debug/\ -*/AppKit/\ -*/Foundation/\ -*/Objective-J/\ -*/*.environment/\ -*/Build/\ -*/.*/\ -*/NS_*.j\ -*/main.j\ -*/.*\ -!*/.xcodecapp-ignore\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f0 \cf0 \ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\b\fs28 \cf0 Preferences\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\fs24 \cf0 \ - -\b0 XcodeCapp supports the following preferences:\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f1 \cf0 {{\NeXTGraphic preferences.png \width6840 \height3560 -}}\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\f0 \cf0 \ -\ -Currently the only warnings generated by XcodeCapp are from using a category in an Objective-J file. Since this warning does not prevent the rest of the file from being parsed, you may choose not to display the Errors panel when category warnings are generated.\ -\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\b\fs28 \cf0 Troubleshooting\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\b0\fs24 \cf0 Ultimately XcodeCapp is a bit of a hack \'97 an elegant hack, but a hack nonetheless. It is doing all kinds of processing behind Xcode\'92s back, so to speak, and occasionally Xcode may get out of sync with your source files. In such cases, you can reset XcodeCapp by selecting \'93Reset Project\'94 from the XcodeCapp menu. This will remove and then regenerate the Xcode project and .XcodeSupport folder. If the Xcode project is open in Xcode it will be closed before being removed. \ -\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\b\fs28 \cf0 Credits\ -\ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural - -\b0\fs24 \cf0 XcodeCapp was originally written by Francisco Tolmasky.\ -The Cocoa port was written by Antoine Mercadal and Aparajita Fishman.\ -XcodeCapp (Cocoa version, formerly known as XcodeCapp-cocoa) was originally built for the Archipel Project (archipelproject.org) and is now part of Cappuccino.} \ No newline at end of file diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/actions.png b/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/actions.png deleted file mode 100644 index ad22c38b3..000000000 Binary files a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/actions.png and /dev/null differ diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/active.png b/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/active.png deleted file mode 100644 index fae2c2c7b..000000000 Binary files a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/active.png and /dev/null differ diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/error.png b/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/error.png deleted file mode 100644 index f24f3128c..000000000 Binary files a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/error.png and /dev/null differ diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/errors-panel.png b/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/errors-panel.png deleted file mode 100644 index 28e144d12..000000000 Binary files a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/errors-panel.png and /dev/null differ diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/frameworks.png b/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/frameworks.png deleted file mode 100644 index 32ef912a1..000000000 Binary files a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/frameworks.png and /dev/null differ diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/loading-project.png b/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/loading-project.png deleted file mode 100644 index 9b2bed2ff..000000000 Binary files a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/loading-project.png and /dev/null differ diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/menu.png b/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/menu.png deleted file mode 100644 index b0b5f757f..000000000 Binary files a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/menu.png and /dev/null differ diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/open-project-in-xcode.png b/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/open-project-in-xcode.png deleted file mode 100644 index 6b9bd2d2d..000000000 Binary files a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/open-project-in-xcode.png and /dev/null differ diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/outlets-xcode.png b/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/outlets-xcode.png deleted file mode 100644 index 2944758ca..000000000 Binary files a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/outlets-xcode.png and /dev/null differ diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/outlets.png b/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/outlets.png deleted file mode 100644 index 8231afa34..000000000 Binary files a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/outlets.png and /dev/null differ diff --git a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/preferences.png b/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/preferences.png deleted file mode 100644 index 9ee930b80..000000000 Binary files a/Tools/XcodeCapp/XcodeCapp/Resources/help.rtfd/preferences.png and /dev/null differ diff --git a/Tools/XcodeCapp/XcodeCapp/Scripts/mod_pbxproj.py b/Tools/XcodeCapp/XcodeCapp/Scripts/mod_pbxproj.py index 8291e0b23..d79a717de 100755 --- a/Tools/XcodeCapp/XcodeCapp/Scripts/mod_pbxproj.py +++ b/Tools/XcodeCapp/XcodeCapp/Scripts/mod_pbxproj.py @@ -169,20 +169,24 @@ class PBXFileReference(PBXType): } trees = [ - '', - '', - 'BUILT_PRODUCTS_DIR', - 'DEVELOPER_DIR', - 'SDKROOT', - 'SOURCE_ROOT', - ] + '', + '', + 'BUILT_PRODUCTS_DIR', + 'DEVELOPER_DIR', + 'SDKROOT', + 'SOURCE_ROOT', + ] def guess_file_type(self): self.remove('explicitFileType') self.remove('lastKnownFileType') - ext = os.path.splitext(self.get('name', ''))[1] - f_type, build_phase = PBXFileReference.types.get(ext, ('?', None)) + if os.path.isdir(self.get('path')): + f_type = 'folder' + build_phase = None + else: + ext = os.path.splitext(self.get('name', ''))[1] + f_type, build_phase = PBXFileReference.types.get(ext, ('?', None)) self['lastKnownFileType'] = f_type self.build_phase = build_phase @@ -530,7 +534,7 @@ class XcodeProject(PBXDict): if not path: path = os.path.join(os.getcwd(), 'project.pbxproj') - self.pbxproj_path =os.path.abspath(path) + self.pbxproj_path = os.path.abspath(path) self.source_root = os.path.abspath(os.path.join(os.path.split(path)[0], '..')) IterableUserDict.__init__(self, d) @@ -594,7 +598,7 @@ class XcodeProject(PBXDict): if b.add_library_search_paths(paths, recursive): self.modified = True - # TODO: need to return value if project has been modified + # TODO: need to return value if project has been modified def get_obj(self, id): return self.objects.get(id) @@ -604,36 +608,36 @@ class XcodeProject(PBXDict): def get_files_by_os_path(self, os_path, tree='SOURCE_ROOT'): files = [f for f in self.objects.values() if f.get('isa') == 'PBXFileReference' - and f.get('path') == os_path - and f.get('sourceTree') == tree] + and f.get('path') == os_path + and f.get('sourceTree') == tree] return files def get_files_by_name(self, name, parent=None): if parent: files = [f for f in self.objects.values() if f.get('isa') == 'PBXFileReference' - and f.get(name) == name - and parent.has_child(f)] + and f.get(name) == name + and parent.has_child(f)] else: files = [f for f in self.objects.values() if f.get('isa') == 'PBXFileReference' - and f.get(name) == name] + and f.get(name) == name] return files def get_build_files(self, id): files = [f for f in self.objects.values() if f.get('isa') == 'PBXBuildFile' - and f.get('fileRef') == id] + and f.get('fileRef') == id] return files def get_groups_by_name(self, name, parent=None): if parent: groups = [g for g in self.objects.values() if g.get('isa') == 'PBXGroup' - and g.get_name() == name - and parent.has_child(g)] + and g.get_name() == name + and parent.has_child(g)] else: groups = [g for g in self.objects.values() if g.get('isa') == 'PBXGroup' - and g.get_name() == name] + and g.get_name() == name] return groups @@ -666,7 +670,7 @@ class XcodeProject(PBXDict): path = os.path.abspath(path) groups = [g for g in self.objects.values() if g.get('isa') == 'PBXGroup' - and os.path.abspath(g.get('path','/dev/null')) == path] + and os.path.abspath(g.get('path','/dev/null')) == path] return groups @@ -816,12 +820,12 @@ class XcodeProject(PBXDict): results.append(build_file) if abs_path and tree == 'SOURCE_ROOT' and os.path.isfile(abs_path)\ - and file_ref.build_phase == 'PBXFrameworksBuildPhase': + and file_ref.build_phase == 'PBXFrameworksBuildPhase': library_path = os.path.join('$(SRCROOT)', os.path.split(f_path)[0]) self.add_library_search_paths([library_path], recursive=False) if abs_path and tree == 'SOURCE_ROOT' and not os.path.isfile(abs_path)\ - and file_ref.build_phase == 'PBXFrameworksBuildPhase': + and file_ref.build_phase == 'PBXFrameworksBuildPhase': framework_path = os.path.join('$(SRCROOT)', os.path.split(f_path)[0]) self.add_framework_search_paths([framework_path,'$(inherited)'], recursive=False) @@ -1039,11 +1043,11 @@ class XcodeProject(PBXDict): for f in v: filerefs.extend([fr.id for fr in self.objects.values() if fr.get('isa') == 'PBXFileReference' - and fr.get('name') == f]) + and fr.get('name') == f]) buildfiles = [bf for bf in self.objects.values() if bf.get('isa') == 'PBXBuildFile' - and bf.get('fileRef') in filerefs] + and bf.get('fileRef') in filerefs] for bf in buildfiles: if bf.add_compiler_flag(k): @@ -1153,22 +1157,22 @@ class XcodeProject(PBXDict): #root.remove('objects') #remove it to avoid problems sections = [ - ('PBXBuildFile',False), - ('PBXCopyFilesBuildPhase',True), - ('PBXFileReference',False), - ('PBXFrameworksBuildPhase',True), - ('PBXGroup',True), - ('PBXNativeTarget',True), - ('PBXProject',True), - ('PBXResourcesBuildPhase',True), - ('PBXShellScriptBuildPhase',True), - ('PBXSourcesBuildPhase',True), - ('XCBuildConfiguration',True), - ('XCConfigurationList',True), - ('PBXTargetDependency', True), - ('PBXVariantGroup', True), - ('PBXReferenceProxy', True), - ('PBXContainerItemProxy', True)] + ('PBXBuildFile',False), + ('PBXCopyFilesBuildPhase',True), + ('PBXFileReference',False), + ('PBXFrameworksBuildPhase',True), + ('PBXGroup',True), + ('PBXNativeTarget',True), + ('PBXProject',True), + ('PBXResourcesBuildPhase',True), + ('PBXShellScriptBuildPhase',True), + ('PBXSourcesBuildPhase',True), + ('XCBuildConfiguration',True), + ('XCConfigurationList',True), + ('PBXTargetDependency', True), + ('PBXVariantGroup', True), + ('PBXReferenceProxy', True), + ('PBXContainerItemProxy', True)] for section in sections: #iterate over the sections if(self.sections.get(section[0]) == None): @@ -1226,7 +1230,7 @@ class XcodeProject(PBXDict): out.write('"'+XcodeProject.addslashes(root)+'"') if(root in self.uuids): out.write(" /* "+self.uuids[root]+" */"); - + @classmethod def getJSONFromXML(cls, root): result = '' @@ -1252,7 +1256,7 @@ class XcodeProject(PBXDict): for child in root.childNodes: if child.nodeType != Node.ELEMENT_NODE: continue; - + if(i>0): result += "," result += XcodeProject.getJSONFromXML(child); @@ -1266,24 +1270,24 @@ class XcodeProject(PBXDict): break result += data return result; - + @classmethod def Load(cls, path): cls.plutil_path = os.path.join(os.path.split(__file__)[0], 'plutil') - + if not os.path.isfile(XcodeProject.plutil_path): cls.plutil_path = 'plutil' - + if subprocess.call([XcodeProject.plutil_path,'-lint','-s',path]): print 'ERROR: not a valid .pbxproj file' return None - + # load project by converting to JSON and parse p = subprocess.Popen([XcodeProject.plutil_path, '-convert', 'xml1', '-o', '-', path], stdout=subprocess.PIPE) rawXML = p.communicate()[0] - + xml = parseString(rawXML); jsonStr = XcodeProject.getJSONFromXML(xml.getElementsByTagName('dict')[0]); - + tree = json.loads(jsonStr) return XcodeProject(tree, path) diff --git a/Tools/XcodeCapp/XcodeCapp/Scripts/parser.j b/Tools/XcodeCapp/XcodeCapp/Scripts/parser.j index 7709f9dae..127924a4a 100644 --- a/Tools/XcodeCapp/XcodeCapp/Scripts/parser.j +++ b/Tools/XcodeCapp/XcodeCapp/Scripts/parser.j @@ -24,6 +24,7 @@ var FILE = require("file"), OS = require("os"), + stream = require("narwhal/term").stream, SLASH_REPLACEMENT = "∕"; // DIVISION SLASH, Unicode: U+2215 @@ -46,22 +47,12 @@ var errors = [], { ClassDeclarationStatement: function(node, st, c) { - if (node.categoryname) - { - [errors addObject:@{ - @"message": "Categories are not supported yet, ignoring it.", - @"path": node.loc.source, - @"line": node.loc.start.line - }]; - - return; - } - var className = node.classname.name, - superclassname = node.superclassname.name, + superclassname = node.superclassname ? node.superclassname.name : "", declaredOutletsName = [], classInfo = { "name": className, + "category": node.categoryname ? node.categoryname.name : "", "superClass": superclassname, "outlets": [], "actions": [], @@ -101,28 +92,31 @@ var errors = [], methodReturnType = [node.returntype ? node.returntype.name : "id"], methodHasAction = node.action ? "IBAction" : null, selector = selectors[0].name, - actionInformations = {"name": selector, "arguments":[]}; + actionInfo = {"name": selector, "arguments":[]}; - if (methodHasAction && arguments.length == 1) + if (methodHasAction) { - if (st.actionNames.indexOf(selector) !== -1) - raise(node.loc.start, "Action named '" + selector + "' is declared multiple times."); - - st.actionNames.push(selector); - - for (var i = 0; i < arguments.length; i++) + if (arguments.length == 1) { - var argument = arguments[i], - argumentName = argument.identifier.name, - argumentType = argument.type ? argument.type.name : null; + if (st.actionNames.indexOf(selector) !== -1) + raise(node.loc.start, "Action named '" + selector + "' is declared multiple times."); - actionInformations.arguments.push({"type": argumentType, "name": argumentName}); + st.actionNames.push(selector); + + for (var i = 0; i < arguments.length; i++) + { + var argument = arguments[i], + argumentName = argument.identifier.name, + argumentType = argument.type ? argument.type.name : null; + + actionInfo.arguments.push({"type": argumentType, "name": argumentName}); + } + + st.actions.push(actionInfo) } - - st.actions.push(actionInformations) + else + raise(node.loc.start, "Action methods must have exactly one parameter."); } - else if (methodHasAction) - raise(node.loc.start, "Method '" + selector + "' is an action but has more than one parameter."); } } ); @@ -137,25 +131,32 @@ function compile(node, state, visitor) c(node, state); }; -function shadowBaseNameForPath(path) +function shadowBaseNameForPath(projectBasePath, path) { + // Make the path project-relative + path = path.substring(projectBasePath.length + 1, path.length); + // strip the extension and replace slashes return path.substring(0, path.length - 2).replace(/[/]/g, SLASH_REPLACEMENT); } +/* + $1 Project base path + $2 Full project source path +*/ function main(args) { try { - var fileURL = new CFURL(args[1]), - outputDirectory = args[2], - baseFilename = shadowBaseNameForPath(fileURL.path()), + var projectBasePath = args[1], + sourcePath = args[2], + outputDirectory = [projectBasePath stringByAppendingPathComponent:@".XcodeSupport"], + baseFilename = shadowBaseNameForPath(projectBasePath, sourcePath), outputHeaderURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilename + ".h"]), outputImplementationURL = new CFURL([outputDirectory stringByAppendingPathComponent:baseFilename + ".m"]), - source = FILE.read(fileURL, { charset: "UTF-8" }), + source = FILE.read(sourcePath, { charset: "UTF-8" }), flags = ObjectiveJ.Preprocessor.Flags.IncludeDebugSymbols | ObjectiveJ.Preprocessor.Flags.IncludeTypeSignatures, - sourceFile = fileURL.path(), - tokens = ObjectiveJ.acorn.parse(source, { locations:true, sourceFile:sourceFile }), + tokens = ObjectiveJ.acorn.parse(source, { locations:true, sourceFile:sourcePath }), classesInformation = [], ObjectiveCSource = "", ObjectiveCHeader = "", @@ -166,7 +167,6 @@ function main(args) // dump(classesInformation) ObjectiveCHeader += - "#import \n" + "#import \n" + '#import "xcc_general_include.h"\n'; @@ -176,7 +176,10 @@ function main(args) classesInformation.forEach(function(aClass) { // add new class definition - ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ : %@", aClass.name, NSCompatibleClassName(aClass.superClass)]; + if (aClass.superClass) + ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ : %@", aClass.name, NSCompatibleClassName(aClass.superClass)]; + else + ObjectiveCHeader += [CPString stringWithFormat:@"\n@interface %@ (%@)", aClass.name, aClass.category]; // add each outlet in header if (aClass.outlets.length > 0) @@ -215,10 +218,10 @@ function main(args) { [errors addObject:@{ @"message": e.message, - @"path": sourceFile, + @"path": sourcePath, @"line": e.line }]; - + hasErrors = YES; } @@ -226,8 +229,8 @@ function main(args) { var plist = [CPPropertyListSerialization dataFromPropertyList:errors format:CPPropertyListXMLFormat_v1_0]; - print([plist rawString]); - + stream.printError([plist rawString]); + // If there were category warnings, hasErrors is NO, so return a warning status OS.exit(hasErrors ? 1 : 2); } @@ -567,6 +570,7 @@ var NSClasses = { "NSStepper" : YES, "NSStepperCell" : YES, "NSString Application Kit Additions" : YES, + "NSTableCellView" : YES, "NSTableColumn" : YES, "NSTableHeaderCell" : YES, "NSTableHeaderView" : YES, diff --git a/Tools/XcodeCapp/XcodeCapp/Scripts/pbxprojModifier.py b/Tools/XcodeCapp/XcodeCapp/Scripts/pbxprojModifier.py index 8bcc391f8..dabf75448 100755 --- a/Tools/XcodeCapp/XcodeCapp/Scripts/pbxprojModifier.py +++ b/Tools/XcodeCapp/XcodeCapp/Scripts/pbxprojModifier.py @@ -4,135 +4,254 @@ import re import sys from mod_pbxproj import XcodeProject -XCODE_SUPPORT_FOLDER = ".XcodeSupport" -SLASH_REPLACEMENT = u"∕" # DIVISION SLASH Unicode U+2215 -STRING_RE = re.compile(ur"^\s*(.*)\s*$", re.MULTILINE) -FRAMEWORKS_RE = re.compile(ur"^(.+/Frameworks/(?:Debug|Source)/([^/]+))/.+$") -XCC_GENERAL_INCLUDE = u"xcc_general_include.h" +class PBXModifier (object): -def update_general_include(project, projectBasePath, shadowGroup): - xcc_general_include_path = os.path.join(projectBasePath, XCODE_SUPPORT_FOLDER, XCC_GENERAL_INCLUDE) - content = u"" + XCODE_SUPPORT_FOLDER = u".XcodeSupport" + SLASH_REPLACEMENT = u"∕" # DIVISION SLASH Unicode U+2215 + STRING_RE = re.compile(ur"^\s*(.*)\s*$", re.MULTILINE) + FRAMEWORKS_RE = re.compile(ur"^(.+/Frameworks/(?:Debug|Source)/([^/]+))/.+$") + XCC_GENERAL_INCLUDE = u"xcc_general_include.h" - for path in os.listdir(os.path.join(projectBasePath, XCODE_SUPPORT_FOLDER)): - filename = unicode(os.path.basename(path)) + def __init__(self, projectRootPath=None): + self.projectRootPath = projectRootPath + projectName = os.path.basename(projectRootPath) + self.pbxPath = os.path.join(projectRootPath, projectName + u".xcodeproj", u"project.pbxproj") + self.project = XcodeProject.Load(self.pbxPath) - if filename.endswith(".h") and filename != XCC_GENERAL_INCLUDE: - content += u'#include "{0}"\n'.format(filename) + self.shadowGroup = self.project.get_or_create_group(u"Cocoa Classes") + self.sourceGroup = self.project.get_or_create_group(u"Cappuccino Source") + self._frameworksGroup = None - f = open(xcc_general_include_path, "w") - f.write(content.encode("utf-8")) - f.close() + @property + def frameworksGroup(self): + if self._frameworksGroup is None: + self._frameworksGroup = self.project.get_or_create_group(u"Frameworks", parent=self.sourceGroup) - if len(project.get_files_by_os_path(os.path.join(XCODE_SUPPORT_FOLDER, XCC_GENERAL_INCLUDE))) == 0: - project.add_file(xcc_general_include_path, parent=shadowGroup) + return self._frameworksGroup -def file_with_path(path, projectPath, project): - relPath = os.path.relpath(path, projectPath) + def update_general_include(self): + xcc_general_include_path = os.path.join(self.projectRootPath, self.XCODE_SUPPORT_FOLDER, self.XCC_GENERAL_INCLUDE) + content = u"" - for fileRef in [f for f in project.objects.values() if f.get("isa") == "PBXFileReference"]: - filePath = path if fileRef.get("sourceTree") == "" else relPath + for path in os.listdir(os.path.join(self.projectRootPath, self.XCODE_SUPPORT_FOLDER)): + filename = unicode(os.path.basename(path)) - if fileRef.get("path") == filePath: - return fileRef + if filename.endswith(".h") and filename != self.XCC_GENERAL_INCLUDE: + content += u'#include "{0}"\n'.format(filename) - return None + f = open(xcc_general_include_path, "w") + f.write(content.encode("utf-8")) + f.close() -def add_file(project, shadowGroup, sourceGroup, shadowHeaderPath, shadowImplementationPath, sourcePath, projectBasePath): - # Shadow files are always project-relative - if not file_with_path(shadowHeaderPath, projectBasePath, project): - project.add_file(shadowHeaderPath, parent=shadowGroup, tree="SOURCE_ROOT", create_build_files=False) + if len(self.project.get_files_by_os_path(os.path.join(self.XCODE_SUPPORT_FOLDER, self.XCC_GENERAL_INCLUDE))) == 0: + self.project.add_file(xcc_general_include_path, parent=self.shadowGroup) - if not file_with_path(shadowImplementationPath, projectBasePath, project): - project.add_file(shadowImplementationPath, parent=shadowGroup, tree="SOURCE_ROOT", create_build_files=False) + def file_with_path(self, projectSourcePath): + relativePath = os.path.relpath(projectSourcePath, self.projectRootPath) - # If the file is within the project directory, the file reference will be project-relative, otherwise absolute - if sourcePath.startswith(projectBasePath): - tree = "SOURCE_ROOT" - else: - tree = "" + for fileRef in [f for f in self.project.objects.values() if f.get("isa") == "PBXFileReference"]: + filePath = projectSourcePath if fileRef.get("sourceTree") == "" else relativePath - if not file_with_path(sourcePath, projectBasePath, project): - project.add_file(sourcePath, parent=sourceGroup, tree=tree, create_build_files=False) + if fileRef.get("path") == filePath: + return fileRef -def remove_file(project, shadowGroup, sourceGroup, shadowHeaderPath, shadowImplementationPath, sourcePath, projectBasePath): - for path in (shadowHeaderPath, shadowImplementationPath, sourcePath): - fileRef = file_with_path(path, projectBasePath, project) + return None - if fileRef: - project.remove_file(fileRef) + def add_file(self, projectSourcePath, shadowHeaderPath, shadowImplementationPath): + resolvedPath = os.path.realpath(projectSourcePath) -def xml_converter(matchObj): - return "{0}".format(matchObj.group(1).encode('ascii', 'xmlcharrefreplace')) + # Shadow files are always project-relative + if not self.file_with_path(shadowHeaderPath): + self.project.add_file(shadowHeaderPath, parent=self.shadowGroup, tree="SOURCE_ROOT", create_build_files=False) -def convert_unicode_to_xml(path): - """ - mod_pbxproj writes unicode data as utf-8, but since it's an xml plist - all non-ascii characters should be converted to xml character references. - So we do that as a post-processing phase. + if not self.file_with_path(shadowImplementationPath): + self.project.add_file(shadowImplementationPath, parent=self.shadowGroup, tree="SOURCE_ROOT", create_build_files=False) + + # If the file is within the project directory, the file reference will be project-relative, otherwise absolute + if resolvedPath.startswith(self.projectRootPath): + tree = "SOURCE_ROOT" + else: + tree = "" + + if not self.file_with_path(resolvedPath): + relativePath = os.path.relpath(projectSourcePath, self.projectRootPath) + + if relativePath.startswith(u"Frameworks/"): + parent = self.frameworksGroup + else: + parent = self.sourceGroup + + self.project.add_file(resolvedPath, parent=parent, tree=tree, create_build_files=False) + + def remove_file(self, projectSourcePath, shadowHeaderPath, shadowImplementationPath): + resolvedPath = os.path.realpath(projectSourcePath) + + for path in (shadowHeaderPath, shadowImplementationPath, resolvedPath): + fileRef = self.file_with_path(path) + + if fileRef: + self.project.remove_file(fileRef) + + def xml_converter(self, matchObj): + return "{0}".format(matchObj.group(1).encode("ascii", "xmlcharrefreplace")) + + def convert_unicode_to_xml(self): """ - with open(path, 'rb') as f: - content = f.read().decode('utf-8') + mod_pbxproj writes unicode data as utf-8, but since it's an xml plist + all non-ascii characters should be converted to xml character references. + So we do that as a post-processing phase. + """ + with open(self.pbxPath, 'rb') as f: + content = f.read().decode('utf-8') - with open(path, "wb") as f: - content = STRING_RE.sub(xml_converter, content) - f.write(content) + with open(self.pbxPath, "wb") as f: + content = self.STRING_RE.sub(self.xml_converter, content) + f.write(content) -def add_framework_resources(project, framework, resourcesPath): - files = project.get_files_by_os_path(resourcesPath, tree="") + def add_framework_resources(self, framework, resourcesPath): + files = self.project.get_files_by_os_path(resourcesPath, tree="") - if not files: - files = project.add_file(resourcesPath, parent=None, tree="", create_build_files=False) + if not files: + files = self.project.add_file(resourcesPath, parent=None, tree="", create_build_files=False) - if files: - files[0]['name'] = framework + " Resources" + if files: + files[0]['name'] = framework + u" Resources" -def save_project(project, pbxPath): - project.save() - convert_unicode_to_xml(pbxPath) + def compare_file_ids(self, id1, id2): + # A few special cases: + # - Frameworks group always goes last + # - XCC_GENERAL_INCLUDE always goes after another file + # - Frameworks/* file always goes after a non-Frameworks file + obj1 = self.project.get_obj(id1) + name1 = obj1.get("name", obj1.get("path")) + obj2 = self.project.get_obj(id2) + name2 = obj2.get("name", obj2.get("path")) -if __name__ == "__main__": + # Note: the "∕" in "Frameworks∕" is actually Unicode DIVISION_SLASH, not SOLIDUS (forward slash) + if name1 == u"Frameworks" and obj1.get("isa") == u"PBXGroup": + return 1 + elif name2 == u"Frameworks" and obj2.get("isa") == u"PBXGroup": + return -1 + elif name1 == self.XCC_GENERAL_INCLUDE and obj2.get("isa") == u"PBXFileReference": + return 1 + elif name2 == self.XCC_GENERAL_INCLUDE and obj1.get("isa") == u"PBXFileReference": + return -1 + elif name1.startswith(u"Frameworks∕") and not name2.startswith(u"Frameworks∕"): + return 1 + elif name2.startswith(u"Frameworks∕") and not name1.startswith(u"Frameworks∕"): + return -1 - action = sys.argv[1] + return cmp(name1.lower(), name2.lower()) - if action in ("add", "remove"): - projectBasePath = unicode(sys.argv[2]) - projectSourcePath = unicode(sys.argv[3]) - sourcePath = os.path.realpath(projectSourcePath) + def compare_resource_folder_ids(self, id1, id2): + folder1 = self.project.get_obj(id1) + name1 = folder1.get("name", folder1.get("path")) - shadowBasePath = os.path.join(projectBasePath, XCODE_SUPPORT_FOLDER) - shadowBaseName = os.path.splitext(sourcePath)[0].replace(u"/", SLASH_REPLACEMENT) + folder2 = self.project.get_obj(id2) + name2 = folder2.get("name", folder2.get("path")) + + if name1 == u"Resources": + return -1 + elif name2 == u"Resources": + return 1 + + return cmp(name1.lower(), name2.lower()) + + def sort_project(self): + # Sort the files alphabetically in our groups. + for group in (self.sourceGroup, self.shadowGroup, self._frameworksGroup): + if group is None: + continue + + group.get("children").data.sort(cmp=self.compare_file_ids) + + # Move resource folders to the top, Resources at the very top + root_ids = self.project.root_group.get("children").data + folder_ids = [] + + for id in root_ids: + item = self.project.get_obj(id) + name = item.get("name", item.get("path")) + + if name.endswith(u"Resources") and item.get("lastKnownFileType") == "folder": + folder_ids.append(id) + + folder_ids.sort(cmp=self.compare_resource_folder_ids, reverse=True) + + for id in folder_ids: + index = root_ids.index(id) + del root_ids[index] + root_ids.insert(0, id) + + def save_project(self): + self.sort_project() + self.project.save() + self.convert_unicode_to_xml() + + def update_project_with_source(self, action, projectSourcePath): + relativePath = os.path.relpath(projectSourcePath, self.projectRootPath) + + shadowBasePath = os.path.join(projectRootPath, self.XCODE_SUPPORT_FOLDER) + shadowBaseName = os.path.splitext(relativePath)[0].replace(u"/", self.SLASH_REPLACEMENT) shadowHeaderPath = os.path.join(shadowBasePath, shadowBaseName + ".h") shadowImplementationPath = os.path.join(shadowBasePath, shadowBaseName + ".m") - projectName = os.path.basename(projectBasePath) - pbxPath = os.path.join(projectBasePath, projectName + ".xcodeproj", "project.pbxproj") - - project = XcodeProject.Load(pbxPath) - - shadowGroup = project.get_or_create_group("Classes") - sourceGroup = project.get_or_create_group("Sources") if action == "add": - fileRef = file_with_path(shadowHeaderPath, projectBasePath, project) + fileRef = self.file_with_path(shadowHeaderPath) if not fileRef: - update_general_include(project, projectBasePath, shadowGroup) - add_file(project, shadowGroup, sourceGroup, shadowHeaderPath, shadowImplementationPath, sourcePath, projectBasePath) + self.update_general_include() + self.add_file(projectSourcePath, shadowHeaderPath, shadowImplementationPath) - match = FRAMEWORKS_RE.match(projectSourcePath) + match = self.FRAMEWORKS_RE.match(projectSourcePath) if match: framework = match.group(2) - resourcesPath = os.path.realpath(os.path.join(match.group(1), "Resources")) + resourcesPath = os.path.realpath(os.path.join(match.group(1), u"Resources")) if os.path.isdir(resourcesPath): - add_framework_resources(project, framework, resourcesPath) - - save_project(project, pbxPath) + self.add_framework_resources(framework, resourcesPath) elif action == "remove": - update_general_include(project, projectBasePath, shadowGroup) - remove_file(project, shadowGroup, sourceGroup, shadowHeaderPath, shadowImplementationPath, sourcePath, projectBasePath) - save_project(project, pbxPath) + self.update_general_include() + self.remove_file(projectSourcePath, shadowHeaderPath, shadowImplementationPath) + + +# +# Possible ways to call this script: +# +# "add" projectRootPath file +# "remove" projectRootPath file +# "update" projectRootPath action file... [action file...] +# +# File paths are full project paths, no resolved symlinks. +# action is "add" or "remove". +# +if __name__ == "__main__": + + action = sys.argv[1] + projectRootPath = unicode(sys.argv[2]) + modifier = PBXModifier(projectRootPath) + + if action in ("add", "remove"): + projectSourcePath = unicode(sys.argv[3]) + modifier.update_project_with_source(action, projectSourcePath) + modifier.save_project() + + elif action == "update": + # When the action is "update", it is followed by "add" or "remove", followed by 1+ paths + args = sys.argv[3:] + action = unicode(args.pop(0)) + + while len(args): + arg = unicode(args.pop(0)) + + if arg in ("add", "remove"): + action = arg + continue + else: + modifier.update_project_with_source(action, arg) + + modifier.save_project() diff --git a/Tools/XcodeCapp/XcodeCapp/TNXCodeCapp.h b/Tools/XcodeCapp/XcodeCapp/TNXCodeCapp.h deleted file mode 100644 index 7f623c60a..000000000 --- a/Tools/XcodeCapp/XcodeCapp/TNXCodeCapp.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * This file is a part of program XcodeCapp - * Copyright (C) 2011 Antoine Mercadal () - * - * 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 . - */ - -#import -#import - -enum XCCAPIMode { - kXCCAPIModeAuto = 0, - kXCCAPIModeFile, - kXCCAPIModeFolder -}; - -extern NSString * const XCCDidPopulateProjectNotification; -extern NSString * const XCCConversionDidStartNotification; -extern NSString * const XCCConversionDidStopNotification; -extern NSString * const XCCListeningDidStartNotification; - - -@interface TNXcodeCapp : NSObject - -@property NSString* XcodeSupportProjectPath; -@property NSString* currentProjectPath; -@property NSString* currentAPIMode; -@property BOOL supportsFileBasedListening; -@property BOOL reactToInodeModification; -@property BOOL isListening; -@property BOOL supportsFileLevelAPI; -@property BOOL isUsingFileLevelAPI; -@property BOOL isLoadingProject; -@property NSMutableArray* errorList; - -@property (unsafe_unretained) IBOutlet NSTableView *errorTable; -@property (strong) IBOutlet NSPanel *errorsPanel; -@property (strong) IBOutlet NSArrayController *errorListController; - -- (IBAction)openErrorsPanel:(id)sender; -- (IBAction)clearErrors:(id)sender; -- (IBAction)openErrorInEditor:(id)sender; -- (IBAction)openInXcode:(id)aSender; -- (IBAction)resetProject:(id)aSender; - -- (void)start; -- (void)stop; -- (void)listenToProjectAtPath:(NSString *)path; -- (void)updateLastEventId:(uint64_t)eventId; -- (BOOL)pathMatchesIgnoredPaths:(NSString*)aPath; -- (BOOL)isObjjFile:(NSString *)path; -- (BOOL)isXibFile:(NSString *)path; -- (BOOL)isXCCIgnoreFile:(NSString *)path; -- (void)tidyShadowedFiles; -- (void)handleFileModificationAtPath:(NSString*)path notify:(BOOL)shouldNotify; -- (void)handleFileRemovalAtPath:(NSString*)path; -- (void)updateUserDefaultsWithLastEventId; -- (BOOL)hasErrors; - -@end - -@interface TNXcodeCapp (SnowLeopard) - -- (void)updateLastModificationDate:(NSDate *)date forPath:(NSString *)path; -- (NSDate *)lastModificationDateForPath:(NSString *)path; - -@end diff --git a/Tools/XcodeCapp/XcodeCapp/TNXcodeCapp.m b/Tools/XcodeCapp/XcodeCapp/TNXcodeCapp.m deleted file mode 100644 index 4d1a112ed..000000000 --- a/Tools/XcodeCapp/XcodeCapp/TNXcodeCapp.m +++ /dev/null @@ -1,1252 +0,0 @@ -/* - * This file is a part of program XcodeCapp - * Copyright (C) 2011 Antoine Mercadal () - * - * 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 . - */ - -#import - -#import "TNXcodeCapp.h" -#import "FSEventCallback.h" -#import "UserDefaults.h" - -#include "macros.h" - -enum XCCTaskReturnType { - kTaskReturnTypeNone, - kTaskReturnTypeStdOut, - kTaskReturnTypeStdError -}; -typedef enum XCCTaskReturnType XCCTaskReturnType; - -enum XCCLineSpecifier { - kLineSpecifierNone, - kLineSpecifierColon, - kLineSpecifierMinusL, - kLineSpecifierPlus -}; -typedef enum XCCLineSpecifier XCCLineSpecifier; - -enum { - XCCStatusCodeError = 1, - XCCStatusCodeWarning = 2 -}; - -const char *XCCCloseProjectScript = - "tell application \"Xcode\"\n" - "set docs to (document of every window)\n" - "repeat with doc in docs\n" - "if class of doc is workspace document then\n" - "set docURL to file of doc\n" - "set docPath to docURL as text\n" - "set docPath to POSIX path of docPath\n" - "if docPath begins with \"%@\" then\n" - "close doc\n" - "return\n" - "end if\n" - "end if\n" - "end repeat\n" - "end tell"; - -NSString * const XCCDidPopulateProjectNotification = @"XCCDidPopulateProjectNotification"; -NSString * const XCCConversionDidStartNotification = @"XCCConversionDidStartNotification"; -NSString * const XCCConversionDidStopNotification = @"XCCConversionDidStopNotification"; -NSString * const XCCListeningDidStartNotification = @"XCCListeningDidStartNotification"; - -NSString * const XCCSlashReplacement = @"∕"; // DIVISION SLASH, Unicode: U+2215 -NSString * const XCCBashPath = @"/bin/bash"; -NSString * const XCCZShPath = @"/bin/zsh"; - -NSString * const XCCDirectoriesToIgnorePattern = @"^(?:Build|F(?:rameworks|oundation)|AppKit|Objective-J|(?:Browser|CommonJS)\\.environment|Resources|XcodeSupport|.+\\.xcodeproj)$"; -NSRegularExpression * XCCDirectoriesToIgnoreRegex = nil; - -NSArray *XCCDefaultIgnoredPathRegexes = nil; - - -@interface TNXcodeCapp () - -@property FSEventStreamRef stream; -@property NSFileManager *fm; -@property NSMutableArray *ignoredPathRegexes; -@property NSNumber *lastEventId; -@property NSString *parserPath; -@property NSString *XcodeSupportPBXPath; -@property NSString *XcodeTemplatePBXPath; -@property NSString *profilePath; -@property NSString *shellPath; -@property NSString *PBXModifierScriptPath; -@property NSString *closeProjectScriptSource; -@property NSString *supportPath; -@property NSDate *appStartedTimestamp; -@property NSMutableDictionary *pathModificationDates; -@property NSMutableDictionary *projectPathsForSourcePaths; -@property NSString *xcodecappIgnorePath; - -@end - - -@implementation TNXcodeCapp - -#pragma mark - Initialization - -+ (void)initialize -{ - if (self != [TNXcodeCapp class]) - return; - - NSError *error = NULL; - XCCDirectoriesToIgnoreRegex = [NSRegularExpression regularExpressionWithPattern:XCCDirectoriesToIgnorePattern options:0 error:&error]; - - NSArray *defaultIgnoredPaths = @[ - @"*/Frameworks/", - @"!*/Frameworks/Debug/", - @"*/AppKit/", - @"*/Foundation/", - @"*/Objective-J/", - @"*/*.environment/", - @"*/Build/", - @"*/.*/", - @"*/NS_*.j", - @"*/main.j", - @"*/.*", - @"!*/.xcodecapp-ignore" - ]; - - XCCDefaultIgnoredPathRegexes = [self parseIgnorePaths:defaultIgnoredPaths]; -} - -- (id)init -{ - self = [super init]; - - if (self) - { - self.errorList = [NSMutableArray arrayWithCapacity:10]; - self.fm = [NSFileManager defaultManager]; - self.ignoredPathRegexes = [NSMutableArray new]; - self.parserPath = [[[NSBundle mainBundle] sharedSupportPath] stringByAppendingPathComponent:@"parser.j"]; - self.closeProjectScriptSource = [NSString stringWithUTF8String:XCCCloseProjectScript]; - self.lastEventId = [[NSUserDefaults standardUserDefaults] objectForKey:kDefaultLastEventId]; - self.appStartedTimestamp = [NSDate date]; - self.projectPathsForSourcePaths = [NSMutableDictionary new]; - self.xcodecappIgnorePath = @""; - - self.isListening = NO; - self.isUsingFileLevelAPI = NO; - self.isLoadingProject = NO; - - SInt32 versionMajor = 0; - SInt32 versionMinor = 0; - Gestalt(gestaltSystemVersionMajor, &versionMajor); - Gestalt(gestaltSystemVersionMinor, &versionMinor); - - self.supportsFileLevelAPI = versionMajor >= 10 && versionMinor >= 7; - // Uncomment to simulate 10.6 mode - // self.supportsFileLevelAPI = NO; - - [self configureFileAPI]; - [self getShellProfilePath]; - - NSUserNotificationCenter.defaultUserNotificationCenter.delegate = self; - [GrowlApplicationBridge setGrowlDelegate:self]; - } - - return self; -} - -- (void)start -{ - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - - if (![defaults boolForKey:kDefaultXCCReopenLastProject]) - return; - - NSString *lastOpenedPath = [defaults objectForKey:kDefaultLastOpenedPath]; - - if (lastOpenedPath) - { - if ([self.fm fileExistsAtPath:lastOpenedPath]) - [self listenToProjectAtPath:lastOpenedPath]; - else - [defaults removeObjectForKey:kDefaultLastOpenedPath]; - } -} - -/*! - Stops the event listener, clears state - */ -- (void)stop -{ - [self stopEventStream]; - [self clearErrors:self]; - [self updateUserDefaultsWithLastEventId]; - [[NSUserDefaults standardUserDefaults] synchronize]; - self.currentProjectPath = nil; - [self.ignoredPathRegexes removeAllObjects]; -} - - -#pragma mark - Project Management - -/*! - Create Xcode project and .XcodeSupport directory if necessary. - - @return YES if both exist -*/ -- (BOOL)prepareXcodeSupportProject -{ - self.XcodeTemplatePBXPath = [[NSBundle mainBundle] pathForResource:@"project" ofType:@"pbxproj"]; - - NSString *projectName = [self.currentProjectPath.lastPathComponent stringByAppendingString:@".xcodeproj"]; - - self.XcodeSupportProjectPath = [self.currentProjectPath stringByAppendingPathComponent:projectName]; - self.supportPath = [self.currentProjectPath stringByAppendingPathComponent:@".XcodeSupport"]; - self.XcodeSupportPBXPath = [self.XcodeSupportProjectPath stringByAppendingPathComponent:@"project.pbxproj"]; - self.PBXModifierScriptPath = [[[NSBundle mainBundle] sharedSupportPath] stringByAppendingPathComponent:@"pbxprojModifier.py"]; - - // If either the project or the support directory are missing, recreate them both to ensure they are in sync - BOOL projectExists, projectIsDirectory; - projectExists = [self.fm fileExistsAtPath:self.XcodeSupportProjectPath isDirectory:&projectIsDirectory]; - - BOOL supportExists, supportIsDirectory; - supportExists = [self.fm fileExistsAtPath:self.supportPath isDirectory:&supportIsDirectory]; - - if (!projectExists || !projectIsDirectory || !supportExists) - { - if (projectExists) - [self.fm removeItemAtPath:self.XcodeSupportProjectPath error:nil]; - - [self.fm createDirectoryAtPath:self.XcodeSupportProjectPath withIntermediateDirectories:YES attributes:nil error:nil]; - NSLog(@"Xcode support folder created at: %@", self.XcodeSupportProjectPath); - - DLog(@"Copying project.pbxproj from %@ to %@", self.XcodeTemplatePBXPath, self.XcodeSupportProjectPath); - [self.fm copyItemAtPath:self.XcodeTemplatePBXPath toPath:self.XcodeSupportPBXPath error:nil]; - - DLog(@"Reading the content of the project.pbxproj"); - NSMutableString *PBXContent = [NSMutableString stringWithContentsOfFile:self.XcodeSupportPBXPath encoding:NSUTF8StringEncoding error:nil]; - - [PBXContent writeToFile:self.XcodeSupportPBXPath atomically:YES encoding:NSUTF8StringEncoding error:nil]; - DLog(@"PBX file adapted to the project"); - } - - // If the project did not exist, reset the XcodeSupport directory to force the new empty project to be populated - if (!supportExists || !supportIsDirectory || !projectExists) - { - if (supportExists) - [self.fm removeItemAtPath:self.supportPath error:nil]; - - [self.fm createDirectoryAtPath:self.supportPath withIntermediateDirectories:YES attributes:nil error:nil]; - DLog(@"Created XcodeSupport directory"); - } - - return projectExists && supportExists; -} - -/*! - Create and initialize the Xcode project. -*/ -- (void)populateXcodeProject:(BOOL)shouldNotify -{ - // First populate with all non-framework code - [self populateXcodeProjectWithProjectRelativePath:@""]; - - // Now populate with any user source debug frameworks - [self populateXcodeProjectWithProjectRelativePath:@"Frameworks/Debug"]; - - // Now populate with any source frameworks - [self populateXcodeProjectWithProjectRelativePath:@"Frameworks/Source"]; - - // Now populate resources - [self populateXcodeProjectWithProjectRelativePath:@"Resources"]; - - if (shouldNotify) - [[NSNotificationCenter defaultCenter] postNotificationName:XCCDidPopulateProjectNotification object:self userInfo:nil]; -} - -- (void)populateXcodeProjectWithProjectRelativePath:(NSString *)aProjectPath -{ - NSError *error = NULL; - NSString *projectPath = [self.currentProjectPath stringByAppendingPathComponent:aProjectPath]; - - NSArray *urls = [self.fm contentsOfDirectoryAtURL:[NSURL fileURLWithPath:projectPath.stringByResolvingSymlinksInPath] - includingPropertiesForKeys:@[NSURLIsDirectoryKey, NSURLIsSymbolicLinkKey] - options:NSDirectoryEnumerationSkipsHiddenFiles | NSDirectoryEnumerationSkipsPackageDescendants | NSDirectoryEnumerationSkipsSubdirectoryDescendants - error:&error]; - - if (!urls) - return; - - for (NSURL *url in urls) - { - NSString *filename = url.lastPathComponent; - - NSString *projectRelativePath = [aProjectPath stringByAppendingPathComponent:filename]; - NSString *realPath = url.path; - NSURL *resolvedURL = url; - - NSNumber *isDirectory, *isSymlink; - [url getResourceValue:&isSymlink forKey:NSURLIsSymbolicLinkKey error:nil]; - - if (isSymlink.boolValue == YES) - { - resolvedURL = [url URLByResolvingSymlinksInPath]; - - if ([resolvedURL checkResourceIsReachableAndReturnError:nil]) - { - filename = resolvedURL.lastPathComponent; - realPath = resolvedURL.path; - } - else - continue; - } - - [resolvedURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; - - if (isDirectory.boolValue == YES) - { - if ([XCCDirectoriesToIgnoreRegex numberOfMatchesInString:filename options:0 range:NSMakeRange(0, filename.length)] > 0) - continue; - - // If the resolved path is not within the project directory, add a mapping to it - // so we can map the resolved path back to the project directory later. - if (isSymlink.boolValue == YES) - { - NSString *fullProjectPath = [self.currentProjectPath stringByAppendingPathComponent:projectRelativePath]; - - if (![realPath hasPrefix:fullProjectPath]) - self.projectPathsForSourcePaths[realPath] = fullProjectPath; - } - - [self populateXcodeProjectWithProjectRelativePath:projectRelativePath]; - continue; - } - - if ([self pathMatchesIgnoredPaths:realPath]) - continue; - - NSString *projectSourcePath = [self.currentProjectPath stringByAppendingPathComponent:projectRelativePath]; - - if ([self isObjjFile:filename] || [self isXibFile:filename]) - { - NSString *processedPath; - - if ([self isObjjFile:filename]) - processedPath = [[self shadowBasePathForSourcePath:realPath] stringByAppendingPathExtension:@"h"]; - else - processedPath = [realPath.stringByDeletingPathExtension stringByAppendingPathExtension:@"cib"]; - - if (![self.fm fileExistsAtPath:processedPath]) - [self processModifiedFileAtPath:realPath projectSourcePath:projectSourcePath notify:YES]; - } - } -} - -- (void)listenToProjectAtPath:(NSString *)path -{ - self.isLoadingProject = YES; - - [self notifyUserWithTitle:@"Loading project…" message:path.lastPathComponent]; - - [self clearErrors:self]; - self.currentProjectPath = path; - self.xcodecappIgnorePath = [self.currentProjectPath stringByAppendingPathComponent:@".xcodecapp-ignore"]; - self.projectPathsForSourcePaths = [NSMutableDictionary new]; - [self computeIgnoredPaths]; - - BOOL projectNeedsPopulation = [self prepareXcodeSupportProject]; - [self populateXcodeProject:projectNeedsPopulation]; - - self.isLoadingProject = NO; - [self initializeEventStreamWithPath:self.currentProjectPath]; - - [[NSNotificationCenter defaultCenter] postNotificationName:XCCListeningDidStartNotification object:self userInfo:nil]; - [[NSUserDefaults standardUserDefaults] setObject:self.currentProjectPath forKey:kDefaultLastOpenedPath]; - - [self notifyUserWithTitle:@"Project loaded" message:self.currentProjectPath.lastPathComponent]; -} - -- (IBAction)openInXcode:(id)aSender -{ - BOOL isDirectory, opened = YES; - BOOL exists = [self.fm fileExistsAtPath:self.XcodeSupportProjectPath isDirectory:&isDirectory]; - - if (exists && isDirectory) - { - DLog(@"Opening Xcode project at: %@", self.XcodeSupportProjectPath); - opened = [[NSWorkspace sharedWorkspace] openFile:self.XcodeSupportProjectPath]; - } - - if (!exists || !isDirectory || !opened) - { - NSString *text; - - if (!opened) - text = @"The project exists, but failed to open."; - else - text = [NSString stringWithFormat:@"%@ %@.", self.XcodeSupportProjectPath, !exists ? @"does not exist" : @"is not an Xcode project"]; - - [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; - NSInteger response = NSRunAlertPanel(@"The project could not be opened.", @"%@\n\nWould you like to regenerate the project?", @"Yes", @"No", nil, text); - - if (response == NSAlertDefaultReturn) - [self resetProject:self]; - } -} - -- (IBAction)resetProject:(id)aSender -{ - NSString *projectPath = self.currentProjectPath; - - [self stop]; - - // Close the project in Xcode if it's open - NSString *source = [NSString stringWithFormat:self.closeProjectScriptSource, projectPath]; - NSAppleScript *script = [[NSAppleScript alloc] initWithSource:source]; - - NSAppleEventDescriptor *descriptor; - descriptor = [script executeAndReturnError:nil]; - - [self.fm removeItemAtPath:self.XcodeSupportProjectPath error:nil]; - [self.fm removeItemAtPath:self.supportPath error:nil]; - - [self listenToProjectAtPath:projectPath]; -} - -#pragma mark - Event Stream - -/*! - Initializes the FSEvent stream - - @param path the path of the folder to watch - */ -- (void)initializeEventStreamWithPath:(NSString *)path -{ - if (self.isListening) - return; - - [self stopEventStream]; - - NSMutableArray *pathsToWatch = [NSMutableArray arrayWithObject:path]; - FSEventStreamCreateFlags flags = 0; - - if (self.supportsFileBasedListening) - { - DLog(@"Initializing the FSEventStream at file level (clean)"); - flags = kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents; - } - else - { - NSLog(@"Initializing the FSEventStream at folder level (dirty)"); - flags = kFSEventStreamCreateFlagUseCFTypes; - } - - NSArray *directoriesToWatch = @[@"", @"Frameworks/Debug", @"Frameworks/Source"]; - - for (NSString *directory in directoriesToWatch) - { - NSString *fullPath = [self.currentProjectPath stringByAppendingPathComponent:directory]; - - BOOL exists, isDirectory; - exists = [self.fm fileExistsAtPath:fullPath isDirectory:&isDirectory]; - - if (exists && isDirectory) - [self watchSymlinkedDirectoriesAtPath:directory pathsToWatch:pathsToWatch]; - } - - void *appPointer = (__bridge void *)self; - FSEventStreamContext context = { 0, appPointer, NULL, NULL, NULL }; - CFTimeInterval latency = 2.0; - - self.stream = FSEventStreamCreate(NULL, &fsevents_callback, &context, (__bridge CFArrayRef) pathsToWatch, - self.lastEventId.unsignedLongLongValue, latency, flags); - - FSEventStreamScheduleWithRunLoop(self.stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); - FSEventStreamStart(self.stream); - self.isListening = YES; -} - -- (void)watchSymlinkedDirectoriesAtPath:(NSString *)projectPath pathsToWatch:(NSMutableArray *)pathsToWatch -{ - NSString *fullProjectPath = [self.currentProjectPath stringByAppendingPathComponent:projectPath]; - NSError *error = NULL; - - NSArray *urls = [self.fm contentsOfDirectoryAtURL:[NSURL fileURLWithPath:fullProjectPath] - includingPropertiesForKeys:@[NSURLIsDirectoryKey, NSURLIsSymbolicLinkKey] - options:NSDirectoryEnumerationSkipsHiddenFiles | NSDirectoryEnumerationSkipsPackageDescendants | NSDirectoryEnumerationSkipsSubdirectoryDescendants - error:&error]; - - for (NSURL *url in urls) - { - NSNumber *isSymlink; - [url getResourceValue:&isSymlink forKey:NSURLIsSymbolicLinkKey error:nil]; - - if (isSymlink.boolValue == YES) - { - NSURL *resolvedURL = [url URLByResolvingSymlinksInPath]; - - if ([resolvedURL checkResourceIsReachableAndReturnError:nil]) - { - NSNumber *isDirectory; - [resolvedURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; - - if (isDirectory.boolValue == YES) - { - NSString *filename = resolvedURL.lastPathComponent; - - if ([XCCDirectoriesToIgnoreRegex numberOfMatchesInString:filename options:0 range:NSMakeRange(0, filename.length)] == 0) - [pathsToWatch addObject:resolvedURL.path]; - } - } - } - } -} - -- (void)stopEventStream -{ - if (self.stream) - { - FSEventStreamStop(self.stream); - FSEventStreamUnscheduleFromRunLoop(self.stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); - FSEventStreamInvalidate(self.stream); - FSEventStreamRelease(self.stream); - self.stream = NULL; - } - - self.isListening = NO; -} - -/*! - Choose the API mode according to default - */ -- (void)configureFileAPI -{ - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - - if (!self.supportsFileLevelAPI) - { - DLog(@"System doesn't support file level API"); - [defaults setObject:[NSNumber numberWithInt:kXCCAPIModeFolder] forKey:kDefaultXCCAPIMode]; - } - - switch ([defaults integerForKey:kDefaultXCCAPIMode]) - { - case kXCCAPIModeAuto: - self.supportsFileBasedListening = self.supportsFileLevelAPI; - break; - - case kXCCAPIModeFile: - self.supportsFileBasedListening = YES; - break; - - case kXCCAPIModeFolder: - self.supportsFileBasedListening = NO; - break; - } - - if (self.supportsFileBasedListening) - { - DLog(@"using 10.7+ mode listening (clean)"); - - self.currentAPIMode = @"File level (Lion)"; - self.isUsingFileLevelAPI = YES; - self.reactToInodeModification = [defaults boolForKey:kDefaultXCCReactMode]; - } - else - { - DLog(@"using 10.6 mode listening (dirty)"); - self.reactToInodeModification = NO; - self.currentAPIMode = @"Folder level (Snow Leopard)"; - self.isUsingFileLevelAPI = NO; - } -} - -/*! - Update the last event ID. We use a method because - this is called from outside the class, in the FSEvent callback. - - @param eventId the current event ID value -*/ -- (void)updateLastEventId:(uint64_t)eventId -{ - self.lastEventId = [NSNumber numberWithUnsignedLongLong:eventId]; -} - -/*! - Updates the user defaults with the last recorded event Id. -*/ -- (void)updateUserDefaultsWithLastEventId -{ - if (self.lastEventId && self.lastEventId.longLongValue != 0) - [[NSUserDefaults standardUserDefaults] setObject:self.lastEventId forKey:kDefaultLastEventId]; -} - - -#pragma mark - Shell Helpers - -/*! - Run an NSTask with the given arguments - - @param arguments NSArray containing the NSTask arguments - @return NSarray containing the return status (int) and the response (string) - */ -- (NSDictionary *)runTaskWithLaunchPath:(NSString *)launchPath arguments:(NSArray *)arguments returnType:(XCCTaskReturnType)returnType -{ - NSTask *task = [NSTask new]; - - task.launchPath = launchPath; - task.arguments = arguments; - task.standardOutput = [NSPipe pipe]; - task.standardError = [NSPipe pipe]; - [task launch]; - - if (returnType != kTaskReturnTypeNone) - { - [task waitUntilExit]; - - NSData *data = [[(returnType == kTaskReturnTypeStdOut ? task.standardOutput : task.standardError) fileHandleForReading] availableData]; - NSString *response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - NSNumber *status = [NSNumber numberWithInt:task.terminationStatus]; - - return @{ @"status":status, @"response":response }; - } - else - { - return @{ @"status":@0, @"response":@"" }; - } -} - -- (void)getShellProfilePath -{ - NSString* myShell = [[[NSProcessInfo processInfo] environment] objectForKey:@"SHELL"]; - self.shellPath = myShell ? myShell : XCCBashPath; - self.profilePath = @""; - NSArray *profiles; - NSString *path; - - if ([self.shellPath hasSuffix:@"/bash"]) - { - profiles = @[@"~/.bash_profile", @"~/.bashrc", @"~/.profile"]; - } - else if ([self.shellPath hasSuffix:@"/zsh"]) - { - profiles = @[@"~/.zshrc", @"~/.profile"]; - } - else - { - [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; - NSRunAlertPanel(@"Unsupported shell.", @"You are using %@ as your shell. XcodeCapp requires either bash or zsh to run.", nil, nil, nil, self.shellPath); - - [[NSRunningApplication currentApplication] terminate]; - return; - } - - for (NSString *profile in profiles) - { - path = profile.stringByExpandingTildeInPath; - - if ([self.fm fileExistsAtPath:path]) - { - self.profilePath = path; - return; - } - } -} - -#pragma mark - Event Handlers - -/*! - Handle a file modification. If it's a .j or xib/nib, - perform the appropriate conversion. If it's .xcodecapp-ignore, it will - update the list of ignored files. - - @param fullPath The full path of the modified file - @param shouldNotify If YES, Growl notifications will be displayed -*/ -- (void)handleFileModificationAtPath:(NSString*)path notify:(BOOL)shouldNotify -{ - if (![self isXibFile:path] && ![self isObjjFile:path] && ![self isXCCIgnoreFile:path]) - return; - - if ([self pathMatchesIgnoredPaths:path] || ![self.fm fileExistsAtPath:path]) - return; - - NSString *projectPath = [self projectPathForSourcePath:path]; - [self processModifiedFileAtPath:path projectSourcePath:projectPath notify:shouldNotify]; -} - -- (BOOL)processModifiedFileAtPath:(NSString *)realSourcePath projectSourcePath:(NSString *)projectSourcePath notify:(BOOL)shouldNotify -{ - DLog(@"Processing modified file: %@", realSourcePath); - - BOOL success = YES; - - NSArray *arguments = nil; - NSArray *pbxArguments = nil; - NSString *response = nil; - NSString *projectRelativePath = [projectSourcePath substringFromIndex:self.currentProjectPath.length + 1]; - NSString *notificationTitle = nil; - NSString *notificationMessage = projectRelativePath.lastPathComponent; - - [[NSNotificationCenter defaultCenter] postNotificationName:XCCConversionDidStartNotification object:self]; - - // Remove all errors for the path being processed - NSIndexSet *matchingErrors = [self.errorList indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) - { - return [[obj valueForKey:@"path"] isEqualToString:realSourcePath]; - }]; - - [self.errorListController removeObjectsAtArrangedObjectIndexes:matchingErrors]; - - if ([self isXibFile:realSourcePath]) - { - // nib2cib can take a while to run, show a message while the conversion is happening - [self notifyUserWithTitle:@"Converting xib…" message:projectRelativePath.lastPathComponent]; - - arguments = @[ - @"-c", - [NSString stringWithFormat:@"source '%@'; nib2cib --no-colors '%@'", self.profilePath, realSourcePath], - @"" - ]; - - notificationTitle = @"Xib converted"; - } - else if ([self isObjjFile:realSourcePath]) - { - arguments = @[ - @"-c", - [NSString stringWithFormat:@"(source '%@'; objj '%@' '%@' '%@') 2>&1", - self.profilePath, - self.parserPath, - realSourcePath, - self.supportPath] - ]; - - pbxArguments = @[ - @"-c", - [NSString stringWithFormat:@"(source '%@'; python '%@' add '%@' '%@') 2>&1", - self.profilePath, - self.PBXModifierScriptPath, - self.currentProjectPath, - projectSourcePath] - ]; - - notificationTitle = @"Objective-J source processed"; - } - else if ([self isXCCIgnoreFile:realSourcePath]) - { - [self computeIgnoredPaths]; - notificationTitle = @"Parsed .xcodecapp-ignore"; - notificationMessage = @"Ignored paths updated"; - arguments = nil; - } - - // Run the task and get the response if needed - if (arguments) - { - DLog(@"Running conversion task..."); - - NSDictionary *taskResult = [self runTaskWithLaunchPath:self.shellPath - arguments:arguments - returnType:[self isObjjFile:realSourcePath] ? kTaskReturnTypeStdOut : kTaskReturnTypeStdError]; - - NSInteger status = [taskResult[@"status"] intValue]; - response = taskResult[@"response"]; - - DLog(@"Conversion task result/response: %ld/%@", status, response); - - if (status != 0) - { - success = NO; - - if (response.length == 0) - response = @"An unspecified error occurred"; - - if ([self isXibFile:realSourcePath]) - { - notificationTitle = @"Error converting xib"; - - NSString *message = [NSString stringWithFormat:@"%@\n%@", realSourcePath.lastPathComponent, response]; - [self.errorListController addObject:@{ @"message":message, @"path":realSourcePath, @"status":taskResult[@"status"] }]; - } - else - { - notificationTitle = [(status == XCCStatusCodeError ? @"Error" : @"Warning") stringByAppendingString:@" parsing Objective-J source"]; - - NSArray *errors = [response propertyList]; - - for (NSDictionary *error in errors) - { - NSMutableDictionary *newError = [error mutableCopy]; - newError[@"message"] = [NSString stringWithFormat:@"%@, line %d\n%@", [error[@"path"] lastPathComponent], [error[@"line"] intValue], error[@"message"]]; - newError[@"status"] = taskResult[@"status"]; - [self.errorListController addObject:newError]; - } - } - - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - - if ((status == XCCStatusCodeError && [defaults boolForKey:kDefaultXCCAutoOpenErrorsPanelOnErrors]) || - (status == XCCStatusCodeWarning && [defaults boolForKey:kDefaultXCCAutoOpenErrorsPanelOnWarnings])) - { - [self openErrorsPanel:self]; - } - - [self notifyUserWithTitle:notificationTitle message:notificationMessage]; - } - else if (!self.isLoadingProject) - { - [self notifyUserWithTitle:notificationTitle message:notificationMessage]; - } - } - - if (pbxArguments) - { - DLog(@"Running update PBX task..."); - - NSDictionary *taskResult; - taskResult = [self runTaskWithLaunchPath:self.shellPath arguments:pbxArguments returnType:kTaskReturnTypeStdOut]; - - DLog(@"Update PBX Task result/response: %@/%@", taskResult[@"status"], taskResult[@"response"]); - } - - [[NSNotificationCenter defaultCenter] postNotificationName:XCCConversionDidStopNotification object:self]; - DLog(@"Processed: %@", realSourcePath); - - return success; -} - -/*! - Handle a file deletion. If it's a .j, it will - remove the shadowed .h file. If it's .xcodecapp-ignore - it will reset the list of ignored files. - - @param fullPath the full path of the modified file - @param shouldNotify if YES, Growl notifications will be displayed -*/ -- (void)handleFileRemovalAtPath:(NSString*)path -{ - if ([self pathMatchesIgnoredPaths:path] || [self.fm fileExistsAtPath:path]) - return; - - if ([self isObjjFile:path]) - { - [self removeReferencesToSourcePath:path]; - [self notifyUserWithTitle:@"Removed Objective-J file" message:path.lastPathComponent]; - } - else if ([self isXCCIgnoreFile:path]) - [self computeIgnoredPaths]; -} - -#pragma mark - Source Files Management - -- (BOOL)isObjjFile:(NSString *)path -{ - return [path.pathExtension.lowercaseString isEqual:@"j"]; -} - -- (BOOL)isXibFile:(NSString *)path -{ - NSString *extension = path.pathExtension.lowercaseString; - - if ([extension isEqual:@"xib"] || [extension isEqual:@"nib"]) - { - // Xcode creates temp files called ~.xib. Filter those out. - NSString *baseFilename = path.lastPathComponent.stringByDeletingPathExtension; - - return [baseFilename characterAtIndex:baseFilename.length - 1] != '~'; - } - - return NO; -} - -- (BOOL)isXCCIgnoreFile:(NSString *)path -{ - return [path isEqualToString:self.xcodecappIgnorePath]; -} - -- (NSString *)projectPathForSourcePath:(NSString *)path -{ - NSString *base = path.stringByDeletingLastPathComponent; - NSString *projectPath = self.projectPathsForSourcePaths[base]; - - return projectPath ? [projectPath stringByAppendingPathComponent:path.lastPathComponent] : path; -} - -#pragma mark - Shadow Files Management - -- (NSString *)shadowBasePathForSourcePath:(NSString *)path -{ - return [self.supportPath stringByAppendingPathComponent:[path.stringByDeletingPathExtension stringByReplacingOccurrencesOfString:@"/" withString:XCCSlashReplacement]]; -} - -- (NSString *)sourcePathForShadowPath:(NSString *)path -{ - path = [path stringByReplacingOccurrencesOfString:XCCSlashReplacement withString:@"/"]; - return [path.stringByDeletingPathExtension stringByAppendingPathExtension:@"j"]; -} - -/*! - Clean up any shadow files and PBX entries related to given the Cappuccino source file path -*/ -- (void)removeReferencesToSourcePath:(NSString *)sourcePath -{ - NSString *shadowBasePath = [self shadowBasePathForSourcePath:sourcePath]; - NSString *shadowHeaderPath = [shadowBasePath stringByAppendingPathExtension:@"h"]; - NSString *shadowImplementationPath = [shadowBasePath stringByAppendingPathExtension:@"m"]; - - DLog(@"Removing shadow header file: %@", shadowHeaderPath); - [self.fm removeItemAtPath:shadowHeaderPath error:nil]; - - DLog(@"Removing shadow implementation file: %@", shadowImplementationPath); - [self.fm removeItemAtPath:shadowImplementationPath error:nil]; - - DLog(@"Removing PBX reference"); - NSString *projectSourcePath = [self projectPathForSourcePath:sourcePath]; - - NSArray *pbxArguments = @[ - @"-c", - [NSString stringWithFormat:@"(source %@; python %@ remove '%@' '%@') 2>&1", - self.profilePath, - self.PBXModifierScriptPath, - self.currentProjectPath, - projectSourcePath] - ]; - - NSDictionary *taskResult; - - taskResult = [self runTaskWithLaunchPath:self.shellPath arguments:pbxArguments returnType:kTaskReturnTypeStdOut]; - - DLog(@"PBX Reference removal status/response: %@/%@", taskResult[@"status"], taskResult[@"response"]); -} - -/*! - Clean the support folder according to files present in given path -*/ -- (void)tidyShadowedFiles -{ - NSArray *subpaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.supportPath error:nil]; - - for (NSString *path in subpaths) - { - if ([path.pathExtension isEqual:@"m"] || [path.lastPathComponent isEqualToString:@"xcc_general_include.h"]) - continue; - - NSString *sourcePath = [self sourcePathForShadowPath:path]; - - if (![self.fm fileExistsAtPath:sourcePath]) - { - [self removeReferencesToSourcePath:sourcePath]; - - if (!self.supportsFileLevelAPI && [self respondsToSelector:@selector(updateLastModificationDate:forPath:)]) - [self updateLastModificationDate:nil forPath:sourcePath]; - } - } -} - -#pragma mark - XCC Ignore management - -+ (NSString *)globToRegexPattern:(NSString *)glob -{ - NSMutableString *regex = [glob mutableCopy]; - - if ([regex characterAtIndex:0] == '!') - [regex deleteCharactersInRange:NSMakeRange(0, 1)]; - - [regex replaceOccurrencesOfString:@"." - withString:@"\\." - options:0 - range:NSMakeRange(0, [regex length])]; - - [regex replaceOccurrencesOfString:@"*" - withString:@".*" - options:0 - range:NSMakeRange(0, [regex length])]; - - // If the glob ends with "/", match that directory and anything below it. - if ([regex characterAtIndex:regex.length - 1] == '/') - [regex replaceCharactersInRange:NSMakeRange(regex.length - 1, 1) withString:@"(?:/.*)?"]; - - return [NSString stringWithFormat:@"^%@$", regex]; -} - -+ (NSArray *)parseIgnorePaths:(NSArray *)paths -{ - NSMutableArray *parsedPaths = [NSMutableArray array]; - NSError *error = NULL; - NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet]; - - for (NSString *pattern in paths) - { - if ([pattern stringByTrimmingCharactersInSet:whitespace].length == 0) - continue; - - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:[self globToRegexPattern:pattern] options:0 error:&error]; - [parsedPaths addObject:@{ @"regex": regex, @"exclude": @([pattern characterAtIndex:0] != '!') }]; - } - - return parsedPaths; -} - -/*! - Compute the ignored paths according to any existing .xcodecapp-ignore file -*/ -- (void)computeIgnoredPaths -{ - self.ignoredPathRegexes = [XCCDefaultIgnoredPathRegexes mutableCopy]; - NSString *ignorePath = [self.currentProjectPath stringByAppendingPathComponent:@".xcodecapp-ignore"]; - - if ([self.fm fileExistsAtPath:ignorePath]) - { - NSString *ignoreFileContent = [NSString stringWithContentsOfFile:ignorePath encoding:NSUTF8StringEncoding error:nil]; - NSArray *ignoredPatterns = [ignoreFileContent componentsSeparatedByString:@"\n"]; - NSArray *parsedPaths = [[self class] parseIgnorePaths:ignoredPatterns]; - [self.ignoredPathRegexes addObjectsFromArray:parsedPaths]; - } - - DLog(@"Ignoring file paths: %@", self.ignoredPathRegexes); -} - -- (BOOL)pathMatchesIgnoredPaths:(NSString*)aPath -{ - BOOL ignore = NO; - NSRange range = NSMakeRange(0, aPath.length); - - for (NSDictionary *ignoreInfo in self.ignoredPathRegexes) - { - BOOL matches = [ignoreInfo[@"regex"] numberOfMatchesInString:aPath options:0 range:range] > 0; - - if (matches) - ignore = [ignoreInfo[@"exclude"] boolValue]; - } - - return ignore; -} - -#pragma mark - Errors - -- (IBAction)openErrorsPanel:(id)aSender -{ - [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; - [self.errorsPanel makeKeyAndOrderFront:nil]; -} - -- (IBAction)openErrorInEditor:(id)sender -{ - id info = self.errorListController.selection; - - NSString *path = [info valueForKey:@"path"]; - - if (path == NSNoSelectionMarker) - return; - - if ([self isObjjFile:path]) - { - [self openObjjFile:path line:[[info valueForKey:@"line"] intValue]]; - } - else // xib - { - NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; - [workspace openFile:path]; - } - - [self.errorsPanel orderOut:self]; -} - -- (void)openObjjFile:(NSString *)path line:(NSInteger)line -{ - NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; - - NSString *app, *type; - BOOL success = [workspace getInfoForFile:path application:&app type:&type]; - - if (!success) - { - NSBeep(); - return; - } - - NSBundle *bundle = [NSBundle bundleWithPath:app]; - NSString *identifier = bundle.bundleIdentifier; - NSString *executablePath = nil; - XCCLineSpecifier lineSpecifier = kLineSpecifierNone; - - if ([identifier hasPrefix:@"com.sublimetext."]) - { - lineSpecifier = kLineSpecifierColon; - executablePath = [[bundle sharedSupportPath] stringByAppendingPathComponent:@"bin/subl"]; - } - else if ([identifier isEqualToString:@"com.barebones.textwrangler"]) - { - lineSpecifier = kLineSpecifierColon; - executablePath = [[bundle bundlePath] stringByAppendingPathComponent:@"Contents/Helpers/edit"]; - } - else if ([identifier isEqualToString:@"com.barebones.bbedit"]) - { - lineSpecifier = kLineSpecifierColon; - executablePath = [[bundle bundlePath] stringByAppendingPathComponent:@"Contents/Helpers/bbedit"]; - } - else if ([identifier isEqualToString:@"com.macromates.textmate"]) // TextMate 1.x - { - lineSpecifier = kLineSpecifierMinusL; - executablePath = [[bundle sharedSupportPath] stringByAppendingPathComponent:@"Support/bin/mate"]; - } - else if ([identifier hasPrefix:@"com.macromates.TextMate"]) // TextMate 2.x - { - lineSpecifier = kLineSpecifierMinusL; - executablePath = [bundle pathForResource:@"mate" ofType:@""]; - } - else if ([identifier isEqualToString:@"com.chocolatapp.Chocolat"]) - { - lineSpecifier = kLineSpecifierMinusL; - executablePath = [[bundle sharedSupportPath] stringByAppendingPathComponent:@"choc"]; - } - else if ([identifier isEqualToString:@"org.vim.MacVim"]) - { - lineSpecifier = kLineSpecifierPlus; - executablePath = @"/usr/local/bin/mvim"; - } - else if ([identifier isEqualToString:@"org.gnu.Aquamacs"]) - { - if ([self.fm isExecutableFileAtPath:@"/usr/bin/aquamacs"]) - executablePath = @"/usr/bin/aquamacs"; - else if ([self.fm isExecutableFileAtPath:@"/usr/local/bin/aquamacs"]) - executablePath = @"/usr/local/bin/aquamacs"; - } - else if ([identifier isEqualToString:@"com.apple.dt.Xcode"]) - { - executablePath = [[bundle bundlePath] stringByAppendingPathComponent:@"Contents/Developer/usr/bin/xed"]; - } - - if (!executablePath || ![self.fm isExecutableFileAtPath:executablePath]) - { - [workspace openFile:path]; - return; - } - - NSArray *args; - - switch (lineSpecifier) - { - case kLineSpecifierNone: - args = @[path]; - break; - - case kLineSpecifierColon: - args = @[[NSString stringWithFormat:@"%1$@:%2$ld", path, line]]; - break; - - case kLineSpecifierMinusL: - args = @[@"-l", [NSString stringWithFormat:@"%ld", line], path]; - break; - - case kLineSpecifierPlus: - args = @[[NSString stringWithFormat:@"+%ld", line], path]; - break; - } - - [self runTaskWithLaunchPath:executablePath arguments:args returnType:kTaskReturnTypeNone]; -} - -- (IBAction)clearErrors:(id)sender -{ - [self.errorList removeAllObjects]; - self.errorListController.content = self.errorList; -} - -- (BOOL)hasErrors -{ - NSInteger index = [self.errorList indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) - { - return [[obj valueForKey:@"status"] intValue] == XCCStatusCodeError; - }]; - - return index != NSNotFound; -} - -#pragma mark - User notifications - -- (NSString *)applicationNameForGrowl -{ - return @"XcodeCapp"; -} - -- (void)notifyUserWithTitle:(NSString *)aTitle message:(NSString *)aMessage -{ - if ([NSUserNotificationCenter class]) - { - NSUserNotification *note = [NSUserNotification new]; - note.title = aTitle; - note.informativeText = aMessage; - - [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:note]; - } - else - { - [GrowlApplicationBridge notifyWithTitle:aTitle - description:aMessage - notificationName:GROWL_NOTIFICATIONS_DEFAULT - iconData:nil - priority:0 - isSticky:NO - clickContext:nil]; - } -} - -- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification -{ - // Notification Center may decide not to show a notification. We always want them to show. - return YES; -} - -@end - - -@implementation TNXcodeCapp (SnowLeopard) - -- (void)updateLastModificationDate:(NSDate *)date forPath:(NSString *)path -{ - if (!self.pathModificationDates) - { - self.pathModificationDates = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:kDefaultPathModificationDates] mutableCopy]; - - if (!self.pathModificationDates) - self.pathModificationDates = [NSMutableDictionary new]; - } - - if (date) - [self.pathModificationDates setObject:date forKey:path]; - else - [self.pathModificationDates removeObjectForKey:path]; - - [[NSUserDefaults standardUserDefaults] setObject:self.pathModificationDates forKey:kDefaultPathModificationDates]; -} - -- (NSDate *)lastModificationDateForPath:(NSString *)path -{ - if (!self.pathModificationDates) - { - self.pathModificationDates = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:kDefaultPathModificationDates] mutableCopy]; - - if (!self.pathModificationDates) - self.pathModificationDates = [NSMutableDictionary new]; - } - - if ([self.pathModificationDates valueForKey:path] != nil) - return [self.pathModificationDates valueForKey:path]; - else - return self.appStartedTimestamp; -} - -@end diff --git a/Tools/XcodeCapp/XcodeCapp/UserDefaults.h b/Tools/XcodeCapp/XcodeCapp/UserDefaults.h index 8cbfc8098..8811b97f4 100644 --- a/Tools/XcodeCapp/XcodeCapp/UserDefaults.h +++ b/Tools/XcodeCapp/XcodeCapp/UserDefaults.h @@ -15,7 +15,7 @@ extern NSString * const kDefaultLastEventId; extern NSString * const kDefaultFirstLaunch; extern NSString * const kDefaultFirstLaunchVersion; extern NSString * const kDefaultXCCAPIMode; -extern NSString * const kDefaultXCCReactMode; +extern NSString * const kDefaultXCCReactToInodeMod; extern NSString * const kDefaultXCCReopenLastProject; extern NSString * const kDefaultXCCAutoOpenErrorsPanelOnWarnings; extern NSString * const kDefaultXCCAutoOpenErrorsPanelOnErrors; @@ -23,5 +23,8 @@ extern NSString * const kDefaultXCCProjectHistory; extern NSString * const kDefaultLastOpenedPath; extern NSString * const kDefaultPathModificationDates; extern NSString * const kDefaultMaxRecentProjects; +extern NSString * const kDefaultLogLevel; +extern NSString * const kDefaultAutoOpenXcodeProject; +extern NSString * const kDefaultShowProcessingNotices; #endif diff --git a/Tools/XcodeCapp/XcodeCapp/UserDefaults.m b/Tools/XcodeCapp/XcodeCapp/UserDefaults.m index 6aa6cf68a..7d0c5651a 100644 --- a/Tools/XcodeCapp/XcodeCapp/UserDefaults.m +++ b/Tools/XcodeCapp/XcodeCapp/UserDefaults.m @@ -13,7 +13,7 @@ NSString * const kDefaultLastEventId = @"lastEventId"; NSString * const kDefaultFirstLaunch = @"FirstLaunch"; NSString * const kDefaultFirstLaunchVersion = @"firstLaunchVersion"; NSString * const kDefaultXCCAPIMode = @"XCCAPIMode"; -NSString * const kDefaultXCCReactMode = @"XCCReactMode"; +NSString * const kDefaultXCCReactToInodeMod = @"XCCReactMode"; NSString * const kDefaultXCCReopenLastProject = @"XCCReopenLastProject"; NSString * const kDefaultXCCAutoOpenErrorsPanelOnWarnings = @"XCCAutoOpenErrorsPanelOnWarnings"; NSString * const kDefaultXCCAutoOpenErrorsPanelOnErrors = @"XCCAutoOpenErrorsPanelOnErrors"; @@ -21,3 +21,6 @@ NSString * const kDefaultXCCProjectHistory = @"XCCProjectHistory"; NSString * const kDefaultLastOpenedPath = @"LastOpenedPath"; NSString * const kDefaultPathModificationDates = @"pathModificationDates"; NSString * const kDefaultMaxRecentProjects = @"maxRecentProjects"; +NSString * const kDefaultLogLevel = @"logLevel"; +NSString * const kDefaultAutoOpenXcodeProject = @"autoOpenXcodeProject"; +NSString * const kDefaultShowProcessingNotices = @"showProcessingNotices"; diff --git a/Tools/XcodeCapp/XcodeCapp/XcodeCapp-Prefix.pch b/Tools/XcodeCapp/XcodeCapp/XcodeCapp-Prefix.pch index c3ac0cfad..92f4e2158 100644 --- a/Tools/XcodeCapp/XcodeCapp/XcodeCapp-Prefix.pch +++ b/Tools/XcodeCapp/XcodeCapp/XcodeCapp-Prefix.pch @@ -4,4 +4,13 @@ #ifdef __OBJC__ #import + #import "DDLog.h" + +#if DEBUG + #import "DDTTYLogger.h" +#else + #import "DDASLLogger.h" +#endif + + #import "DDLogLevel.h" #endif diff --git a/Tools/XcodeCapp/XcodeCapp/XcodeCapp.h b/Tools/XcodeCapp/XcodeCapp/XcodeCapp.h new file mode 100644 index 000000000..4f70ca1fd --- /dev/null +++ b/Tools/XcodeCapp/XcodeCapp/XcodeCapp.h @@ -0,0 +1,134 @@ +/* + * This file is a part of program XcodeCapp + * Copyright (C) 2011 Antoine Mercadal () + * + * 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 . + */ + +#import +#import + +// FSEvent listening mode we use +enum XCCAPIMode { + kXCCAPIModeAuto = 0, + kXCCAPIModeFile, + kXCCAPIModeFolder +}; + +// Type of output expected from runTaskWithLaunchPath:arguments:returnType: +enum XCCTaskReturnType { + kTaskReturnTypeNone, + kTaskReturnTypeStdOut, + kTaskReturnTypeStdError +}; +typedef enum XCCTaskReturnType XCCTaskReturnType; + +// Status codes returned by support scripts run as tasks +enum { + XCCStatusCodeError = 1, + XCCStatusCodeWarning = 2 +}; + +// Notifications we send +extern NSString * const XCCConversionDidStartNotification; +extern NSString * const XCCConversionDidEndNotification; +extern NSString * const XCCProjectDidFinishLoadingNotification; + + +@interface XcodeCapp : NSObject + +/* + Every time the user opens or closes a project, this is incremented. It is passed to threaded operations + which load the project. These threaded operations generate notifications that get queued up + on the main thread. Because the user may cancel and start a new load while notifications are + still queued from the previous load, we compare the projectId returned by a notification + with the current projectId. If they don't match, we let the notification drain. +*/ +@property NSInteger projectId; + +// Full path to .XcodeSupport +@property NSString *supportPath; + +// Full path to the Cappuccino project root directory +@property NSString *projectPath; + +// Full path to parser.j +@property NSString *parserPath; + +// Full path to pbxprojModifier.py +@property NSString *pbxModifierScriptPath; + +// Full paths to the executables we rely on: jsc, objj, nib2cib, python +@property NSMutableDictionary *executablePaths; + +// Whether the current OS supports file-level FSEvents (10.7+) +@property BOOL supportsFileLevelAPI; + +// Whether we are actually using file-level FSEvents +@property BOOL isUsingFileLevelAPI; + +// Whether we should process source files that have inode-only modifications +@property BOOL reactToInodeModification; + +// Whether we are in the process of loading a project +@property BOOL isLoadingProject; + +// Whether we are currently processing source files +@property BOOL isProcessing; + +// A mapping from full paths to project-relative paths +@property NSMutableDictionary *projectPathsForSourcePaths; + +// A list of errors generated from the current batch of source processing +@property NSMutableArray *errorList; + +// Panel, table and controller used to display errors +@property (strong) IBOutlet NSPanel *errorsPanel; +@property (unsafe_unretained) IBOutlet NSTableView *errorTable; +@property (strong) IBOutlet NSArrayController *errorListController; + +- (IBAction)openErrorsPanel:(id)sender; +- (IBAction)clearErrors:(id)sender; +- (IBAction)openErrorInEditor:(id)sender; +- (IBAction)openProjectInXcode:(id)aSender; +- (IBAction)resetProject:(id)aSender; + +- (BOOL)executablesAreAccessible; +- (void)stop; +- (void)loadProjectAtPath:(NSString *)path; +- (BOOL)pathMatchesIgnoredPaths:(NSString*)aPath; + +- (BOOL)notificationBelongsToCurrentProject:(NSNotification *)note; + +- (BOOL)isObjjFile:(NSString *)path; +- (BOOL)isXibFile:(NSString *)path; +- (BOOL)isXCCIgnoreFile:(NSString *)path; + +- (NSString *)shadowBasePathForProjectSourcePath:(NSString *)path; +- (BOOL)hasErrors; + +- (void)computeIgnoredPaths; +- (BOOL)shouldIgnoreDirectoryNamed:(NSString *)filename; + +- (void)wantUserNotificationWithInfo:(NSDictionary *)info; +- (NSDictionary *)runTaskWithLaunchPath:(NSString *)launchPath arguments:(NSArray *)arguments returnType:(XCCTaskReturnType)returnType; + +@end + +@interface XcodeCapp (SnowLeopard) + +- (void)updateLastModificationDate:(NSDate *)date forPath:(NSString *)path; +- (NSDate *)lastModificationDateForPath:(NSString *)path; + +@end diff --git a/Tools/XcodeCapp/XcodeCapp/XcodeCapp.m b/Tools/XcodeCapp/XcodeCapp/XcodeCapp.m new file mode 100644 index 000000000..842620c23 --- /dev/null +++ b/Tools/XcodeCapp/XcodeCapp/XcodeCapp.m @@ -0,0 +1,1658 @@ +/* + * This file is a part of program XcodeCapp + * Copyright (C) 2011 Antoine Mercadal () + * + * 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 . + */ + +#include + +#import + +#import "XcodeCapp.h" +#import "AppController.h" +#import "FindSourceFilesOperation.h" +#import "Notifications.h" +#import "ProcessSourceOperation.h" +#import "UserDefaults.h" + + +#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_6 +# define kFSEventStreamCreateFlagFileEvents 0x00000010 +# define kFSEventStreamEventFlagItemIsFile 0x00010000 +# define kFSEventStreamEventFlagItemRemoved 0x00000200 +# define kFSEventStreamEventFlagItemCreated 0x00000200 +# define kFSEventStreamEventFlagItemModified 0x00001000 +# define kFSEventStreamEventFlagItemInodeMetaMod 0x00000400 +# define kFSEventStreamEventFlagItemRenamed 0x00000800 +# define kFSEventStreamEventFlagItemFinderInfoMod 0x00002000 +# define kFSEventStreamEventFlagItemChangeOwner 0x00004000 +# define kFSEventStreamEventFlagItemXattrMod 0x00008000 +#endif + + +enum XCCLineSpecifier { + kLineSpecifierNone, + kLineSpecifierColon, + kLineSpecifierMinusL, + kLineSpecifierPlus +}; +typedef enum XCCLineSpecifier XCCLineSpecifier; + +// Used to close the Xcode project if it is open. +static const char *XCCCloseProjectScript = + "tell application \"Xcode\"\n" + "set docs to (document of every window)\n" + "repeat with doc in docs\n" + "if class of doc is workspace document then\n" + "set docURL to file of doc\n" + "set docPath to docURL as text\n" + "set docPath to POSIX path of docPath\n" + "if docPath begins with \"%@\" then\n" + "close doc\n" + "return\n" + "end if\n" + "end if\n" + "end repeat\n" + "end tell"; + +// Where we put the generated Cocoa class files +static NSString * const XCCSupportFolderName = @".XcodeSupport"; + +// We store a compatibility version in .XcodeSupport/Info.plist. +// If the version is less than the version in XcodeCapp's Info.plist, we regenerate .XcodeSupport. +static NSString * const XCCCompatibilityVersionKey = @"XCCCompatibilityVersion"; + +// We replace "/" in a path with this. It looks like "/", +// but is actually an obscure Unicode character we hope no one uses in a filename. +static NSString * const XCCSlashReplacement = @"∕"; // DIVISION SLASH, Unicode: U+2215 + +// When scanning the project, we immediately ignore directories that match this regex. +static NSString * const XCCDirectoriesToIgnorePattern = @"^(?:Build|F(?:rameworks|oundation)|AppKit|Objective-J|(?:Browser|CommonJS)\\.environment|Resources|XcodeSupport|.+\\.xcodeproj)$"; + +// The regex above is used with this predicate for testing. +static NSPredicate * XCCDirectoriesToIgnorePredicate = nil; + +// An array of the default predicates used to ignore paths. +static NSArray *XCCDefaultIgnoredPathPredicates = nil; + + +@interface XcodeCapp () + +// Only used with 10.6 when we don't have file-level FSEvents +@property (nonatomic) NSMutableDictionary *pathModificationDates; + +@property FSEventStreamRef stream; + +// Whether the FSEventStream is started or stopped. +@property BOOL streamStarted; + +// The last FSEvent id we received. This is stored in the user prefs +// so we can get all changes since the last time XcodeCapp was launched. +@property NSNumber *lastEventId; + +@property NSDate *appStartedTimestamp; + +@property NSFileManager *fm; + +// Full path to the .xcodeproj +@property NSString *xcodeProjectPath; + +// An NSString version of XCCCloseProjectScript +@property NSString *closeXcodeProjectScriptSource; + +// Full path to .xcodecapp-ignore +@property NSString *xcodecappIgnorePath; + +// The current array of predicates used to ignore paths +@property NSMutableArray *ignoredPathPredicates; + +// The environment we pass to tasks launched from XcodeCapp +@property NSMutableDictionary *environment; + +// We keep a file descriptor open for the project directory +// so we can locate it if it moves. +@property int projectPathFileDescriptor; + +// Coalesces the modifications that have to be made to the Xcode project +// after changes are made to source files. Keys are the actions "add" or "remove", +// values are arrays of full paths to source files that need to be added or removed. +@property NSMutableDictionary *pbxOperations; + +// A queue for threaded operations to perform +@property NSOperationQueue *operationQueue; + +// We have to declare this because it is referenced by the fsevents_callback function +- (void)handleFSEventsWithPaths:(NSArray *)paths flags:(const FSEventStreamEventFlags[])eventFlags ids:(const FSEventStreamEventId[])eventIds; + +@end + + +void fsevents_callback(ConstFSEventStreamRef streamRef, + void *userData, + size_t numEvents, + void *eventPaths, + const FSEventStreamEventFlags eventFlags[], + const FSEventStreamEventId eventIds[]) +{ + XcodeCapp *xcc = (__bridge XcodeCapp *)userData; + NSArray *paths = (__bridge NSArray *)eventPaths; + + [xcc handleFSEventsWithPaths:paths flags:eventFlags ids:eventIds]; +} + + +@implementation XcodeCapp + +#pragma mark - Initialization + ++ (void)initialize +{ + if (self != [XcodeCapp class]) + return; + + // Initialize static values that can't be initialized in their declarations + + XCCDirectoriesToIgnorePredicate = [NSPredicate predicateWithFormat:@"SELF matches %@", XCCDirectoriesToIgnorePattern]; + + NSArray *defaultIgnoredPaths = @[ + @"*/Frameworks/", + @"!*/Frameworks/Debug/", + @"*/AppKit/", + @"*/Foundation/", + @"*/Objective-J/", + @"*/*.environment/", + @"*/Build/", + @"*/*.xcodeproj/", + @"*/.*/", + @"*/NS_*.j", + @"*/main.j", + @"*/.*", + @"!*/.xcodecapp-ignore" + ]; + + XCCDefaultIgnoredPathPredicates = [self parseIgnorePaths:defaultIgnoredPaths]; +} + +- (id)init +{ + self = [super init]; + + if (self) + { + self.errorList = [NSMutableArray arrayWithCapacity:10]; + self.fm = [NSFileManager defaultManager]; + self.ignoredPathPredicates = [NSMutableArray new]; + self.parserPath = [[NSBundle mainBundle].sharedSupportPath stringByAppendingPathComponent:@"parser.j"]; + self.closeXcodeProjectScriptSource = [NSString stringWithUTF8String:XCCCloseProjectScript]; + self.appStartedTimestamp = [NSDate date]; + self.projectPathsForSourcePaths = [NSMutableDictionary new]; + self.xcodecappIgnorePath = @""; + self.operationQueue = [NSOperationQueue new]; + self.pbxOperations = [NSMutableDictionary new]; + self.executablePaths = [NSMutableDictionary new]; + self.projectPathFileDescriptor = -1; + + [self initTaskEnvironment]; + + self.isUsingFileLevelAPI = NO; + self.isLoadingProject = NO; + self.isProcessing = NO; + + // File-level FSEvents are only supported on 10.7+ + SInt32 versionMajor = 0; + SInt32 versionMinor = 0; + Gestalt(gestaltSystemVersionMajor, &versionMajor); + Gestalt(gestaltSystemVersionMinor, &versionMinor); + + self.supportsFileLevelAPI = versionMajor >= 10 && versionMinor >= 7; + + // Uncomment to simulate 10.6 mode + // self.supportsFileLevelAPI = NO; + + [self configureFileAPI]; + + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; + [GrowlApplicationBridge setGrowlDelegate:self]; + + [self initObservers]; + } + + return self; +} + +- (void)initTaskEnvironment +{ + // Add possible executable paths to PATH + self.environment = [NSProcessInfo processInfo].environment.mutableCopy; + + NSArray *paths = @[@"/usr/local/bin", @"/usr/local/narwhal/bin", @"~/bin".stringByExpandingTildeInPath]; + self.environment[@"PATH"] = [[paths componentsJoinedByString:@":"] stringByAppendingFormat:@":%@", self.environment[@"PATH"]]; + + // Make sure we are using jsc as the narwhal engine! + self.environment[@"NARWHAL_ENGINE"] = @"jsc"; +} + +- (void)initObservers +{ + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + + [center addObserver:self selector:@selector(addSourceToProjectPathMappingHandler:) name:XCCNeedSourceToProjectPathMappingNotification object:nil]; + [center addObserver:self selector:@selector(sourceConversionDidStartHandler:) name:XCCConversionDidStartNotification object:nil]; + [center addObserver:self selector:@selector(sourceConversionDidEndHandler:) name:XCCConversionDidEndNotification object:nil]; + [center addObserver:self selector:@selector(sourceConversionDidGenerateErrorHandler:) name:XCCConversionDidGenerateErrorNotification object:nil]; +} + +#pragma mark - Properties + +- (id)pathModificationDates +{ + if (!_pathModificationDates) + { + _pathModificationDates = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:kDefaultPathModificationDates] mutableCopy]; + + if (!_pathModificationDates) + _pathModificationDates = [NSMutableDictionary new]; + } + + return _pathModificationDates; +} + +#pragma mark - Project Management + +- (void)loadProjectAtPath:(NSString *)path +{ + DDLogInfo(@"Loading project: %@", path); + + self.isLoadingProject = YES; + ++self.projectId; + + [self notifyUserWithTitle:@"Loading project…" message:path.lastPathComponent]; + + self.projectPath = path; + self.xcodecappIgnorePath = [self.projectPath stringByAppendingPathComponent:@".xcodecapp-ignore"]; + self.projectPathsForSourcePaths = [NSMutableDictionary new]; + [self.pbxOperations removeAllObjects]; + self.lastEventId = [[NSUserDefaults standardUserDefaults] objectForKey:kDefaultLastEventId]; + + [self clearErrors:self]; + [self computeIgnoredPaths]; + + [self prepareXcodeSupport]; + [self populateXcodeProject]; + [self waitForOperationQueueToFinishWithSelector:@selector(projectDidFinishLoading)]; +} + +/*! + Create Xcode project and .XcodeSupport directory if necessary. + + @return YES if both exist +*/ +- (BOOL)prepareXcodeSupport +{ + NSString *projectName = [self.projectPath.lastPathComponent stringByAppendingString:@".xcodeproj"]; + + self.xcodeProjectPath = [self.projectPath stringByAppendingPathComponent:projectName]; + self.supportPath = [self.projectPath stringByAppendingPathComponent:XCCSupportFolderName]; + self.pbxModifierScriptPath = [[NSBundle mainBundle].sharedSupportPath stringByAppendingPathComponent:@"pbxprojModifier.py"]; + + // If either the project or the support directory are missing, recreate them both to ensure they are in sync + BOOL projectExists, projectIsDirectory; + projectExists = [self.fm fileExistsAtPath:self.xcodeProjectPath isDirectory:&projectIsDirectory]; + + BOOL supportExists, supportIsDirectory; + supportExists = [self.fm fileExistsAtPath:self.supportPath isDirectory:&supportIsDirectory]; + + if (!projectExists || !projectIsDirectory || !supportExists) + [self createXcodeProject]; + + // If the project did not exist, reset the XcodeSupport directory to force the new empty project to be populated + if (!supportExists || !supportIsDirectory || !projectExists || ![self xcodeSupportIsCompatible]) + [self createXcodeSupportDirectory]; + + return projectExists && supportExists; +} + +- (BOOL)xcodeSupportIsCompatible +{ + double appCompatibilityVersion = [[[NSBundle mainBundle] objectForInfoDictionaryKey:XCCCompatibilityVersionKey] doubleValue]; + + NSString *infoPath = [self.supportPath stringByAppendingPathComponent:@"Info.plist"]; + NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:infoPath]; + NSNumber *projectCompatibilityVersion = info[XCCCompatibilityVersionKey]; + + if (projectCompatibilityVersion == nil) + { + DDLogVerbose(@"No compatibility version in project"); + return NO; + } + + DDLogVerbose(@"XcodeCapp/project compatibility version: %0.1f/%0.1f", projectCompatibilityVersion.doubleValue, appCompatibilityVersion); + + return projectCompatibilityVersion.doubleValue >= appCompatibilityVersion; +} + +- (void)createXcodeProject +{ + if ([self.fm fileExistsAtPath:self.xcodeProjectPath]) + [self.fm removeItemAtPath:self.xcodeProjectPath error:nil]; + + [self.fm createDirectoryAtPath:self.xcodeProjectPath withIntermediateDirectories:YES attributes:nil error:nil]; + + NSString *pbxPath = [self.xcodeProjectPath stringByAppendingPathComponent:@"project.pbxproj"]; + + [self.fm copyItemAtPath:[[NSBundle mainBundle] pathForResource:@"project" ofType:@"pbxproj"] toPath:pbxPath error:nil]; + + NSMutableString *content = [NSMutableString stringWithContentsOfFile:pbxPath encoding:NSUTF8StringEncoding error:nil]; + + [content writeToFile:pbxPath atomically:YES encoding:NSUTF8StringEncoding error:nil]; + + DDLogInfo(@"Xcode support project created at: %@", self.xcodeProjectPath); +} + +- (void)createXcodeSupportDirectory +{ + if ([self.fm fileExistsAtPath:self.supportPath]) + [self.fm removeItemAtPath:self.supportPath error:nil]; + + [self.fm createDirectoryAtPath:self.supportPath withIntermediateDirectories:YES attributes:nil error:nil]; + + NSNumber *appCompatibilityVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:XCCCompatibilityVersionKey]; + NSData *data = [NSPropertyListSerialization dataFromPropertyList:@{ XCCCompatibilityVersionKey:appCompatibilityVersion } + format:NSPropertyListXMLFormat_v1_0 + errorDescription:nil]; + [data writeToFile:[self.supportPath stringByAppendingPathComponent:@"Info.plist"] atomically:YES]; + + DDLogInfo(@".XcodeSupport directory created at: %@", self.supportPath); +} + +- (void)populateXcodeProject +{ + // Populate with all non-framework code + [self populateXcodeProjectWithProjectRelativePath:@""]; + + // Populate with any user source debug frameworks + [self populateXcodeProjectWithProjectRelativePath:@"Frameworks/Debug"]; + + // Populate with any source frameworks + [self populateXcodeProjectWithProjectRelativePath:@"Frameworks/Source"]; + + // Populate resources + [self populateXcodeProjectWithProjectRelativePath:@"Resources"]; +} + +- (void)populateXcodeProjectWithProjectRelativePath:(NSString *)path +{ + FindSourceFilesOperation *op = [[FindSourceFilesOperation alloc] initWithXCC:self projectId:[NSNumber numberWithInteger:self.projectId] path:path]; + [self.operationQueue addOperation:op]; +} + +- (IBAction)openProjectInXcode:(id)aSender +{ + BOOL isDirectory, opened = YES; + BOOL exists = [self.fm fileExistsAtPath:self.xcodeProjectPath isDirectory:&isDirectory]; + + if (exists && isDirectory) + { + DDLogVerbose(@"Opening Xcode project at: %@", self.xcodeProjectPath); + + opened = [[NSWorkspace sharedWorkspace] openFile:self.xcodeProjectPath]; + } + + if (!exists || !isDirectory || !opened) + { + NSString *text; + + if (!opened) + text = @"The project exists, but failed to open."; + else + text = [NSString stringWithFormat:@"%@ %@.", self.xcodeProjectPath, !exists ? @"does not exist" : @"is not an Xcode project"]; + + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + NSInteger response = NSRunAlertPanel(@"The project could not be opened.", @"%@\n\nWould you like to regenerate the project?", @"Yes", @"No", nil, text); + + if (response == NSAlertDefaultReturn) + [self resetProject:self]; + } +} + +- (IBAction)resetProject:(id)aSender +{ + NSString *projectPath = self.projectPath; + + [self stop]; + [self removeSupportFilesAtPath:projectPath]; + [self removeAllCibsAtPath:[projectPath stringByAppendingPathComponent:@"Resources"]]; + [self loadProjectAtPath:projectPath]; +} + +- (void)removeAllCibsAtPath:(NSString *)path +{ + NSArray *paths = [self.fm contentsOfDirectoryAtPath:path error:nil]; + + for (NSString *filePath in paths) + { + if ([filePath.pathExtension.lowercaseString isEqualToString:@"cib"]) + [self.fm removeItemAtPath:[path stringByAppendingPathComponent:filePath] error:nil]; + } +} + +- (void)removeSupportFilesAtPath:(NSString *)projectPath +{ + [self closeXcodeProjectForProject:projectPath]; + + [self.fm removeItemAtPath:self.xcodeProjectPath error:nil]; + [self.fm removeItemAtPath:self.supportPath error:nil]; +} + +- (void)closeXcodeProjectForProject:(NSString *)projectPath +{ + NSString *source = [NSString stringWithFormat:self.closeXcodeProjectScriptSource, projectPath]; + NSAppleScript *script = [[NSAppleScript alloc] initWithSource:source]; + + NSAppleEventDescriptor *descriptor; + descriptor = [script executeAndReturnError:nil]; +} + +- (void)stop +{ + // Increment the projectId to ensure remaining notifications in the queue for the current project are ignored + ++self.projectId; + + self.isLoadingProject = NO; + [self stopEventStream]; + [self.operationQueue cancelAllOperations]; + + self.projectPath = nil; + [self clearErrors:self]; + [self.ignoredPathPredicates removeAllObjects]; + + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +#pragma mark - Processing + +- (void)waitForOperationQueueToFinishWithSelector:(SEL)selector +{ + self.isProcessing = YES; + + [[NSNotificationCenter defaultCenter] postNotificationName:XCCBatchDidStartNotification object:self]; + + // Poll every half second to see if the queue has finished + [NSTimer scheduledTimerWithTimeInterval:0.5 + target:self + selector:@selector(didQueueTimerFinish:) + userInfo:NSStringFromSelector(selector) + repeats:YES]; +} + +- (void)didQueueTimerFinish:(NSTimer *)timer +{ + if (self.operationQueue.operationCount == 0) + { + SEL selector = NSSelectorFromString(timer.userInfo); + + [timer invalidate]; + + // Can't use plain performSelect: here because ARC doesn't know what the return value is + // because the selector is determined at runtime. So we use performSelectorOnMainThread: + // which has no return value. + [self performSelectorOnMainThread:selector withObject:nil waitUntilDone:NO]; + } +} + +- (void)batchDidFinish +{ + if (self.pbxOperations.count) + { + // See pbxprojModifier.py for info on the arguments + NSMutableArray *arguments = [[NSMutableArray alloc] initWithObjects:self.pbxModifierScriptPath, @"update", self.projectPath, nil]; + + for (NSString *action in self.pbxOperations) + { + NSArray *paths = self.pbxOperations[action]; + + if (paths.count) + { + [arguments addObject:action]; + [arguments addObjectsFromArray:paths]; + } + } + + // This task takes less than a second to execute, no need to put it a separate thread + + NSDictionary *taskResult = [self runTaskWithLaunchPath:self.executablePaths[@"python"] + arguments:arguments + returnType:kTaskReturnTypeStdError]; + + NSInteger status = [taskResult[@"status"] intValue]; + NSString *response = taskResult[@"response"]; + + DDLogVerbose(@"Updated Xcode project: [%ld, %@]", status, status ? response : @""); + } + + [self showErrors]; + + // If the event stream was temporarily stopped, restart it + [self startFSEventStream]; + + self.isProcessing = NO; + + [[NSNotificationCenter defaultCenter] postNotificationName:XCCBatchDidEndNotification object:self]; +} + +- (void)projectDidFinishLoading +{ + [self batchDidFinish]; + + DDLogVerbose(@"Project finished loading"); + + self.isLoadingProject = NO; + + [[NSNotificationCenter defaultCenter] postNotificationName:XCCProjectDidFinishLoadingNotification object:self]; + [[NSUserDefaults standardUserDefaults] setObject:self.projectPath forKey:kDefaultLastOpenedPath]; + + [self notifyUserWithTitle:@"Project loaded" message:self.projectPath.lastPathComponent]; + + [self startEventStream]; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:kDefaultAutoOpenXcodeProject]) + [self openProjectInXcode:self]; +} + +#pragma mark - Notification handlers + +/* + NOTE: All methods in this section which end with "Handler" are called + from threaded NSOperations. +*/ + +/* + Notifications sent by operations contain an int projectId. + The notifications are queued up on the main thread. When the main thread + handles them, it first checks to see if the current projectId + matches the notification's projectId. If not, the notification is ignored. +*/ +- (BOOL)notificationBelongsToCurrentProject:(NSNotification *)note +{ + return [note.userInfo[@"projectId"] intValue] == self.projectId; +} + +- (void)addSourceToProjectPathMappingHandler:(NSNotification *)note +{ + [self performSelectorOnMainThread:@selector(addSourceToProjectPathMapping:) withObject:note waitUntilDone:NO]; +} + +- (void)addSourceToProjectPathMapping:(NSNotification *)note +{ + if (![self notificationBelongsToCurrentProject:note]) + return; + + NSDictionary *info = note.userInfo; + NSString *sourcePath = info[@"sourcePath"]; + + DDLogVerbose(@"Adding source to project mapping: %@ -> %@", sourcePath, info[@"projectPath"]); + + self.projectPathsForSourcePaths[info[@"sourcePath"]] = info[@"projectPath"]; +} + +- (void)sourceConversionDidStartHandler:(NSNotification *)note +{ + [self performSelectorOnMainThread:@selector(sourceConversionDidStart:) withObject:note waitUntilDone:NO]; +} + +- (void)sourceConversionDidStart:(NSNotification *)note +{ + if (![self notificationBelongsToCurrentProject:note]) + return; + + NSDictionary *info = note.userInfo; + NSString *projectPath = info[@"path"]; + + DDLogVerbose(@"%@ %@", NSStringFromSelector(_cmd), projectPath); + + [self pruneProcessingErrorsForProjectPath:projectPath]; +} + +- (void)sourceConversionDidGenerateErrorHandler:(NSNotification *)note +{ + [self performSelectorOnMainThread:@selector(sourceConversionDidGenerateError:) withObject:note waitUntilDone:NO]; +} + +- (void)sourceConversionDidGenerateError:(NSNotification *)note +{ + if (![self notificationBelongsToCurrentProject:note]) + return; + + NSMutableDictionary *info = [note.userInfo mutableCopy]; + + DDLogVerbose(@"%@ %@", NSStringFromSelector(_cmd), info[@"path"]); + + [self.errorListController addObject:info]; +} + +- (void)sourceConversionDidEndHandler:(NSNotification *)note +{ + [self performSelectorOnMainThread:@selector(sourceConversionDidEnd:) withObject:note waitUntilDone:NO]; +} + +- (void)sourceConversionDidEnd:(NSNotification *)note +{ + if (![self notificationBelongsToCurrentProject:note]) + return; + + NSString *path = note.userInfo[@"path"]; + + if ([self isObjjFile:path]) + { + NSMutableArray *addPaths = [self.pbxOperations[@"add"] mutableCopy]; + + if (!addPaths) + self.pbxOperations[@"add"] = @[path]; + else + { + [addPaths addObject:path]; + self.pbxOperations[@"add"] = addPaths; + } + } + + DDLogVerbose(@"%@ %@", NSStringFromSelector(_cmd), path); +} + +#pragma mark - Event Stream + +- (void)startEventStream +{ + if (self.stream) + return; + + [self stopEventStream]; + + FSEventStreamCreateFlags flags = kFSEventStreamCreateFlagUseCFTypes | + kFSEventStreamCreateFlagWatchRoot | + kFSEventStreamCreateFlagIgnoreSelf | + kFSEventStreamCreateFlagNoDefer; + + if (self.supportsFileLevelAPI) + flags |= kFSEventStreamCreateFlagFileEvents; + + // Get a file descriptor to the project directory so we can locate it if it moves + self.projectPathFileDescriptor = open(self.projectPath.UTF8String, O_EVTONLY); + + NSArray *pathsToWatch = [self getPathsToWatch]; + + void *appPointer = (__bridge void *)self; + FSEventStreamContext context = { 0, appPointer, NULL, NULL, NULL }; + CFTimeInterval latency = 2.0; + UInt64 lastEventId = self.lastEventId.unsignedLongLongValue; + + self.stream = FSEventStreamCreate(NULL, + &fsevents_callback, + &context, + (__bridge CFArrayRef) pathsToWatch, + lastEventId, + latency, + flags); + + FSEventStreamScheduleWithRunLoop(self.stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + [self startFSEventStream]; + + DDLogVerbose(@"FSEventStream started for paths: %@", pathsToWatch); +} + +- (void)startFSEventStream +{ + if (self.stream && !self.streamStarted) + { + FSEventStreamStart(self.stream); + self.streamStarted = YES; + } +} + +- (NSArray *)getPathsToWatch +{ + NSMutableArray *pathsToWatch = [NSMutableArray arrayWithObject:self.projectPath]; + NSArray *otherPathsToWatch = @[@"", @"Frameworks/Debug", @"Frameworks/Source"]; + + for (NSString *path in otherPathsToWatch) + { + NSString *fullPath = [self.projectPath stringByAppendingPathComponent:path]; + + BOOL exists, isDirectory; + exists = [self.fm fileExistsAtPath:fullPath isDirectory:&isDirectory]; + + if (exists && isDirectory) + [self watchSymlinkedDirectoriesAtPath:path pathsToWatch:pathsToWatch]; + } + + return [pathsToWatch copy]; +} + +- (void)watchSymlinkedDirectoriesAtPath:(NSString *)projectPath pathsToWatch:(NSMutableArray *)pathsToWatch +{ + NSString *fullProjectPath = [self.projectPath stringByAppendingPathComponent:projectPath]; + NSError *error = NULL; + + NSArray *urls = [self.fm contentsOfDirectoryAtURL:[NSURL fileURLWithPath:fullProjectPath] + includingPropertiesForKeys:@[NSURLIsDirectoryKey, NSURLIsSymbolicLinkKey] + options:NSDirectoryEnumerationSkipsHiddenFiles | NSDirectoryEnumerationSkipsPackageDescendants | NSDirectoryEnumerationSkipsSubdirectoryDescendants + error:&error]; + + for (NSURL *url in urls) + { + NSNumber *isSymlink; + [url getResourceValue:&isSymlink forKey:NSURLIsSymbolicLinkKey error:nil]; + + if (isSymlink.boolValue == NO) + continue; + + NSURL *resolvedURL = [url URLByResolvingSymlinksInPath]; + + if (![resolvedURL checkResourceIsReachableAndReturnError:nil]) + continue; + + NSNumber *isDirectory; + [resolvedURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; + + if (isDirectory.boolValue == NO) + continue; + + NSString *path = resolvedURL.path; + NSString *filename = path.lastPathComponent; + + if (![self shouldIgnoreDirectoryNamed:filename] && ![self pathMatchesIgnoredPaths:path]) + { + DDLogVerbose(@"Watching symlinked directory: %@", path); + + [pathsToWatch addObject:path]; + } + } +} + +- (void)stopEventStream +{ + if (self.stream) + { + [self updateUserDefaultsWithLastEventId]; + + [self stopFSEventStream]; + FSEventStreamUnscheduleFromRunLoop(self.stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + FSEventStreamInvalidate(self.stream); + FSEventStreamRelease(self.stream); + self.stream = NULL; + } + + if (self.projectPathFileDescriptor >= 0) + { + close(self.projectPathFileDescriptor); + self.projectPathFileDescriptor = -1; + } +} + +- (void)stopFSEventStream +{ + if (self.stream && self.streamStarted) + { + FSEventStreamStop(self.stream); + self.streamStarted = NO; + } +} + +- (void)configureFileAPI +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + + if (!self.supportsFileLevelAPI) + { + DDLogInfo(@"System doesn't support file level API, user folder level API"); + + [defaults setObject:[NSNumber numberWithInt:kXCCAPIModeFolder] forKey:kDefaultXCCAPIMode]; + } + + switch ([defaults integerForKey:kDefaultXCCAPIMode]) + { + case kXCCAPIModeAuto: + self.isUsingFileLevelAPI = self.supportsFileLevelAPI; + break; + + case kXCCAPIModeFolder: + self.isUsingFileLevelAPI = NO; + break; + } + + self.reactToInodeModification = self.isUsingFileLevelAPI ? [defaults boolForKey:kDefaultXCCReactToInodeMod] : NO; +} + +- (void)updateUserDefaultsWithLastEventId +{ + UInt64 lastEventId = FSEventStreamGetLatestEventId(self.stream); + + // Just in case the stream callback was never called... + if (lastEventId != 0) + self.lastEventId = [NSNumber numberWithUnsignedLongLong:lastEventId]; + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setObject:self.lastEventId forKey:kDefaultLastEventId]; + [defaults synchronize]; +} + +- (NSString *)dumpFSEventFlags:(FSEventStreamEventFlags)flags +{ + BOOL created = (flags & kFSEventStreamEventFlagItemCreated) != 0; + BOOL removed = (flags & kFSEventStreamEventFlagItemRemoved) != 0; + BOOL inodeMetaModified = (flags & kFSEventStreamEventFlagItemInodeMetaMod) != 0; + BOOL renamed = (flags & kFSEventStreamEventFlagItemRenamed) != 0; + BOOL modified = (flags & kFSEventStreamEventFlagItemModified) != 0; + BOOL finderInfoModified = (flags & kFSEventStreamEventFlagItemFinderInfoMod) != 0; + BOOL changedOwner = (flags & kFSEventStreamEventFlagItemChangeOwner) != 0; + BOOL xattrModified = (flags & kFSEventStreamEventFlagItemXattrMod) != 0; + BOOL isFile = (flags & kFSEventStreamEventFlagItemIsFile) != 0; + BOOL isDir = (flags & kFSEventStreamEventFlagItemIsDir) != 0; + BOOL isSymlink = (flags & kFSEventStreamEventFlagItemIsSymlink) != 0; + + NSMutableArray *flagNames = [NSMutableArray new]; + + if (created) + [flagNames addObject:@"created"]; + + if (removed) + [flagNames addObject:@"removed"]; + + if (inodeMetaModified) + [flagNames addObject:@"inode"]; + + if (renamed) + [flagNames addObject:@"renamed"]; + + if (modified) + [flagNames addObject:@"modified"]; + + if (finderInfoModified) + [flagNames addObject:@"Finder info"]; + + if (changedOwner) + [flagNames addObject:@"owner"]; + + if (xattrModified) + [flagNames addObject:@"xattr"]; + + if (isFile) + [flagNames addObject:@"file"]; + + if (isDir) + [flagNames addObject:@"dir"]; + + if (isSymlink) + [flagNames addObject:@"symlink"]; + + return [flagNames componentsJoinedByString:@", "]; +} + +#pragma mark - Event Handlers + +- (void)handleFSEventsWithPaths:(NSArray *)paths flags:(const FSEventStreamEventFlags[])eventFlags ids:(const FSEventStreamEventId[])eventIds +{ + DDLogVerbose(@"FSEvents: %ld path(s)", paths.count); + + [self.pbxOperations removeAllObjects]; + + NSMutableArray *modifiedPaths = [NSMutableArray new]; + NSMutableArray *renamedDirectories = [NSMutableArray new]; + + BOOL needUpdate = NO; + + for (size_t i = 0; i < paths.count; ++i) + { + FSEventStreamEventFlags flags = eventFlags[i]; + NSString *path = [paths[i] stringByStandardizingPath]; + + BOOL rootChanged = (flags & kFSEventStreamEventFlagRootChanged) != 0; + + if (rootChanged) + { + DDLogVerbose(@"Watched path changed: %@", path); + + [self resetProjectForWatchedPath:path]; + return; + } + + BOOL isHistoryDoneSentinalEvent = (flags & kFSEventStreamEventFlagHistoryDone) != 0; + + if (isHistoryDoneSentinalEvent) + { + DDLogVerbose(@"History done sentinal event"); + continue; + } + + BOOL isMountEvent = (flags & kFSEventStreamEventFlagMount) || (flags & kFSEventStreamEventFlagUnmount); + + if (isMountEvent) + { + DDLogVerbose(@"Volume %@: %@", (flags & kFSEventStreamEventFlagMount) ? @"mounted" : @"unmounted", path); + continue; + } + + BOOL needRescan = (flags & kFSEventStreamEventFlagMustScanSubDirs) != 0; + + if (needRescan) + { + // A rescan requires a reset + [self resetProjectForWatchedPath:path]; + + return; + } + + if (self.isUsingFileLevelAPI) + { + // BOOL finderInfoModified = (flags & kFSEventStreamEventFlagItemFinderInfoMod) != 0; + // BOOL changedOwner = (flags & kFSEventStreamEventFlagItemChangeOwner) != 0; + // BOOL xattrModified = (flags & kFSEventStreamEventFlagItemXattrMod) != 0; + // BOOL isSymlink = (flags & kFSEventStreamEventFlagItemIsSymlink) != 0; + BOOL inodeMetaModified = (flags & kFSEventStreamEventFlagItemInodeMetaMod) != 0; + BOOL isFile = (flags & kFSEventStreamEventFlagItemIsFile) != 0; + BOOL isDir = (flags & kFSEventStreamEventFlagItemIsDir) != 0; + BOOL renamed = (flags & kFSEventStreamEventFlagItemRenamed) != 0; + BOOL modified = (flags & kFSEventStreamEventFlagItemModified) != 0; + BOOL created = (flags & kFSEventStreamEventFlagItemCreated) != 0; + BOOL removed = (flags & kFSEventStreamEventFlagItemRemoved) != 0; + + DDLogVerbose(@"FSEvent: %@ (%@)", path, [self dumpFSEventFlags:flags]); + + if (isDir) + { + /* + When a project is opened for the first time after it is created, + we get an event where the first path is a create for the root directory. + In that case all of the paths have been processed, and we ignore the event. + */ + if (created && [path isEqualToString:self.projectPath.stringByResolvingSymlinksInPath]) + return; + + if (renamed && + !(created || removed) && + ![self shouldIgnoreDirectoryNamed:path.lastPathComponent] && + ![self pathMatchesIgnoredPaths:path]) + { + DDLogVerbose(@"Renamed directory: %@", path); + + [renamedDirectories addObject:path]; + } + + continue; + } + else if (isFile && + (created || modified || renamed || removed || (self.reactToInodeModification && inodeMetaModified)) && + [self isSourceFile:path]) + { + DDLogVerbose(@"FSEvent accepted"); + + if ([self.fm fileExistsAtPath:path]) + [modifiedPaths addObject:path]; + + needUpdate = YES; + } + } + else // directory-based listening + { + // We should drop support for Snow Leopard soon. + + BOOL isDirectory = NO; + [self.fm fileExistsAtPath:path isDirectory:&isDirectory]; + + // If for some reason the path is not a directory, + // we don't want to deal with it in this mode. + if (!isDirectory) + continue; + + NSFileManager *fm = [NSFileManager defaultManager]; + NSArray *subpaths = [fm contentsOfDirectoryAtPath:path error:NULL]; + + for (NSString *subpath in subpaths) + { + NSString *fullPath = [path stringByAppendingPathComponent:subpath]; + + if (![self isSourceFile:fullPath]) + continue; + + NSDate *lastModifiedDate = [self lastModificationDateForPath:fullPath]; + NSDictionary *fileAttributes = [fm attributesOfItemAtPath:fullPath error:nil]; + NSDate *fileModDate = [fileAttributes objectForKey:NSFileModificationDate]; + + if ([fileModDate compare:lastModifiedDate] == NSOrderedDescending) + { + [self updateLastModificationDate:fileModDate forPath:fullPath]; + [modifiedPaths addObject:fullPath]; + needUpdate = YES; + } + } + } + } + + // If directories were renamed, we take the easy way out and reset the project + if (renamedDirectories.count) + [self handleRenamedDirectories:renamedDirectories]; + else if (needUpdate) + [self updateSupportFilesWithModifiedPaths:modifiedPaths]; +} + +- (void)handleRenamedDirectories:(NSArray *)directories +{ + // Make sure we don't get any more events while handling these events + [self stopFSEventStream]; + + DDLogVerbose(@"Renamed directories: %@", directories); + + [self tidyShadowedFiles]; + + for (NSString *directory in directories) + { + // If it doesn't exist, it's the old name. Nothing to do. + // If it does exist, populate the project with the directory. + + if ([self.fm fileExistsAtPath:directory]) + { + // If the directory is within the project, we can populate it directly. + // Otherwise we have to start at the top level and repopulate everything. + if ([directory hasPrefix:self.projectPath]) + [self populateXcodeProjectWithProjectRelativePath:[self projectRelativePathForPath:directory]]; + else + { + [self populateXcodeProject]; + + // Since everything has been repopulated, no point in continuing + break; + } + } + } + + [self waitForOperationQueueToFinishWithSelector:@selector(batchDidFinish)]; +} + +- (void)updateSupportFilesWithModifiedPaths:(NSArray *)modifiedPaths +{ + // Make sure we don't get any more events while handling these events + [self stopFSEventStream]; + + DDLogVerbose(@"Modified files: %@", modifiedPaths); + + [self tidyShadowedFiles]; + + for (NSString *path in modifiedPaths) + [self handleFileModificationAtPath:path]; + + [self waitForOperationQueueToFinishWithSelector:@selector(batchDidFinish)]; +} + +- (void)resetProjectForWatchedPath:(NSString *)path +{ + // If a watched path changes we don't have much choice but to reset the project. + [self stopFSEventStream]; + + if ([path isEqualToString:self.projectPath]) + { + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + NSInteger response = NSRunAlertPanel(@"The project moved.", @"Your project directory has moved. Would you like to reload the project or quit XcodeCapp?", @"Reload", @"Quit", nil); + + BOOL shouldQuit = YES; + + if (response == NSAlertDefaultReturn) + { + char newPathBuf[MAXPATHLEN + 1]; + + int result = fcntl(self.projectPathFileDescriptor, F_GETPATH, newPathBuf); + + if (result == 0) + { + self.projectPath = [NSString stringWithUTF8String:newPathBuf]; + shouldQuit = NO; + } + else + NSRunAlertPanel(@"The project can’t be located.", @"I’m sorry Dave, but I don’t know where the project went. I’m afraid I have to quit now.", @"OK, HAL", nil, nil); + } + + if (shouldQuit) + { + [[NSApplication sharedApplication] terminate:self]; + return; + } + } + + [self resetProject:self]; +} + +/*! + Handle a file modification. If it's a .j or xib/nib, + perform the appropriate conversion. If it's .xcodecapp-ignore, it will + update the list of ignored files. + + @param path The full resolved path of the modified file +*/ +- (void)handleFileModificationAtPath:(NSString*)resolvedPath +{ + if (![self.fm fileExistsAtPath:resolvedPath]) + return; + + NSString *projectPath = [self projectPathForSourcePath:resolvedPath]; + + ProcessSourceOperation *op = [[ProcessSourceOperation alloc] initWithXCC:self + projectId:[NSNumber numberWithInteger:self.projectId] + sourcePath:projectPath]; + [self.operationQueue addOperation:op]; +} + +#pragma mark - Shell Helpers + +/*! + Run an NSTask with the given arguments + + @param launchPath The executable to launch + @param arguments NSArray containing the NSTask arguments + @param returnType Determines whether to return stdout, stderr, or nothing in the response + @return NSDictionary containing the return status (NSNumber) and the response (NSString) + */ +- (NSDictionary *)runTaskWithLaunchPath:(NSString *)launchPath arguments:(NSArray *)arguments returnType:(XCCTaskReturnType)returnType +{ + NSTask *task = [NSTask new]; + + task.launchPath = launchPath; + task.arguments = arguments; + task.environment = self.environment; + task.standardOutput = [NSPipe pipe]; + task.standardError = [NSPipe pipe]; + + [task launch]; + + DDLogVerbose(@"Task launched: %@\n%@", launchPath, arguments); + + if (returnType != kTaskReturnTypeNone) + { + [task waitUntilExit]; + + DDLogVerbose(@"Task exited: %@:%d", launchPath, task.terminationStatus); + + NSData *data = [[(returnType == kTaskReturnTypeStdOut ? task.standardOutput : task.standardError) fileHandleForReading] availableData]; + NSString *response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSNumber *status = [NSNumber numberWithInt:task.terminationStatus]; + + return @{ @"status":status, @"response":response }; + } + else + { + return @{ @"status":@0, @"response":@"" }; + } +} + +- (BOOL)executablesAreAccessible +{ + NSArray *executables = @[@"python", @"jsc", @"objj", @"nib2cib"]; + + for (NSString *executable in executables) + { + NSDictionary *response = [self runTaskWithLaunchPath:@"/usr/bin/which" + arguments:@[executable] + returnType:kTaskReturnTypeStdOut]; + + NSString *path = [response[@"response"] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + if (path.length) + self.executablePaths[executable] = path; + else + { + DDLogError(@"Could not find executable '%@' in PATH", executable); + return NO; + } + } + + DDLogVerbose(@"Executable paths: %@", self.executablePaths); + + return YES; +} + +#pragma mark - Source Files Management + +- (BOOL)isObjjFile:(NSString *)path +{ + return [path.pathExtension.lowercaseString isEqual:@"j"]; +} + +- (BOOL)isXibFile:(NSString *)path +{ + NSString *extension = path.pathExtension.lowercaseString; + + if ([extension isEqual:@"xib"] || [extension isEqual:@"nib"]) + { + // Xcode creates temp files called ~.xib. Filter those out. + NSString *baseFilename = path.lastPathComponent.stringByDeletingPathExtension; + + return [baseFilename characterAtIndex:baseFilename.length - 1] != '~'; + } + + return NO; +} + +- (BOOL)isXCCIgnoreFile:(NSString *)path +{ + return [path isEqualToString:self.xcodecappIgnorePath]; +} + +- (BOOL)isSourceFile:(NSString *)path +{ + return ([self isXibFile:path] || [self isObjjFile:path] || [self isXCCIgnoreFile:path]) && ![self pathMatchesIgnoredPaths:path]; +} + +- (NSString *)projectPathForSourcePath:(NSString *)path +{ + NSString *base = path.stringByDeletingLastPathComponent; + NSString *projectPath = self.projectPathsForSourcePaths[base]; + + return projectPath ? [projectPath stringByAppendingPathComponent:path.lastPathComponent] : path; +} + +- (NSString *)projectRelativePathForPath:(NSString *)path +{ + return [path substringFromIndex:self.projectPath.length + 1]; +} + +#pragma mark - Shadow Files Management + +- (NSString *)shadowBasePathForProjectSourcePath:(NSString *)path +{ + if (path.isAbsolutePath) + path = [self projectRelativePathForPath:path]; + + NSString *filename = [path.stringByDeletingPathExtension stringByReplacingOccurrencesOfString:@"/" withString:XCCSlashReplacement]; + + return [self.supportPath stringByAppendingPathComponent:filename]; +} + +- (NSString *)sourcePathForShadowPath:(NSString *)path +{ + NSString *filename = [path stringByReplacingOccurrencesOfString:XCCSlashReplacement withString:@"/"]; + filename = [filename.stringByDeletingPathExtension stringByAppendingPathExtension:@"j"]; + + return [self.projectPath stringByAppendingPathComponent:filename]; +} + +/*! + Clean up any shadow files and PBX entries related to given the Cappuccino source file path +*/ +- (void)removeReferencesToSourcePaths:(NSArray *)sourcePaths +{ + BOOL updateLastModDate = !self.supportsFileLevelAPI && [self respondsToSelector:@selector(updateLastModificationDate:forPath:)]; + + for (NSString *sourcePath in sourcePaths) + { + if (updateLastModDate) + [self updateLastModificationDate:nil forPath:sourcePath]; + + NSString *shadowBasePath = [self shadowBasePathForProjectSourcePath:sourcePath]; + NSString *shadowHeaderPath = [shadowBasePath stringByAppendingPathExtension:@"h"]; + NSString *shadowImplementationPath = [shadowBasePath stringByAppendingPathExtension:@"m"]; + + [self.fm removeItemAtPath:shadowHeaderPath error:nil]; + [self.fm removeItemAtPath:shadowImplementationPath error:nil]; + + [self pruneProcessingErrorsForProjectPath:sourcePath]; + } + + if (sourcePaths.count) + DDLogVerbose(@"Removed shadow references to: %@", sourcePaths); +} + +- (void)tidyShadowedFiles +{ + NSArray *subpaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.supportPath error:nil]; + NSMutableArray *pathsToRemove = [NSMutableArray new]; + + for (NSString *path in subpaths) + { + if (![path.pathExtension isEqual:@"h"] || [path.lastPathComponent isEqualToString:@"xcc_general_include.h"]) + continue; + + NSString *sourcePath = [self sourcePathForShadowPath:path]; + + if (![self.fm fileExistsAtPath:sourcePath]) + [pathsToRemove addObject:sourcePath]; + } + + [self removeReferencesToSourcePaths:pathsToRemove]; + + if (pathsToRemove.count) + self.pbxOperations[@"remove"] = pathsToRemove; +} + +#pragma mark - XCC Ignore management + ++ (NSString *)globToRegexPattern:(NSString *)glob +{ + NSMutableString *regex = [glob mutableCopy]; + + if ([regex characterAtIndex:0] == '!') + [regex deleteCharactersInRange:NSMakeRange(0, 1)]; + + [regex replaceOccurrencesOfString:@"." + withString:@"\\." + options:0 + range:NSMakeRange(0, [regex length])]; + + [regex replaceOccurrencesOfString:@"*" + withString:@".*" + options:0 + range:NSMakeRange(0, [regex length])]; + + // If the glob ends with "/", match that directory and anything below it. + if ([regex characterAtIndex:regex.length - 1] == '/') + [regex replaceCharactersInRange:NSMakeRange(regex.length - 1, 1) withString:@"(?:/.*)?"]; + + return [NSString stringWithFormat:@"^%@$", regex]; +} + ++ (NSArray *)parseIgnorePaths:(NSArray *)paths +{ + NSMutableArray *parsedPaths = [NSMutableArray array]; + NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet]; + + for (NSString *pattern in paths) + { + if ([pattern stringByTrimmingCharactersInSet:whitespace].length == 0) + continue; + + NSString *regexPattern = [self globToRegexPattern:pattern]; + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF matches %@", regexPattern]; + [parsedPaths addObject:@{ @"predicate": predicate, @"exclude": @([pattern characterAtIndex:0] != '!') }]; + } + + return parsedPaths; +} + +/*! + Compute the ignored paths according to any existing .xcodecapp-ignore file +*/ +- (void)computeIgnoredPaths +{ + self.ignoredPathPredicates = [XCCDefaultIgnoredPathPredicates mutableCopy]; + NSString *ignorePath = [self.projectPath stringByAppendingPathComponent:@".xcodecapp-ignore"]; + + if ([self.fm fileExistsAtPath:ignorePath]) + { + NSString *ignoreFileContent = [NSString stringWithContentsOfFile:ignorePath encoding:NSUTF8StringEncoding error:nil]; + NSArray *ignoredPatterns = [ignoreFileContent componentsSeparatedByString:@"\n"]; + NSArray *parsedPaths = [[self class] parseIgnorePaths:ignoredPatterns]; + [self.ignoredPathPredicates addObjectsFromArray:parsedPaths]; + } + + DDLogVerbose(@"Ignoring file paths: %@", self.ignoredPathPredicates); +} + +- (BOOL)pathMatchesIgnoredPaths:(NSString*)aPath +{ + BOOL ignore = NO; + + for (NSDictionary *ignoreInfo in self.ignoredPathPredicates) + { + BOOL matches = [ignoreInfo[@"predicate"] evaluateWithObject:aPath]; + + if (matches) + ignore = [ignoreInfo[@"exclude"] boolValue]; + } + + return ignore; +} + +- (BOOL)shouldIgnoreDirectoryNamed:(NSString *)filename +{ + return [XCCDirectoriesToIgnorePredicate evaluateWithObject:filename]; +} + +#pragma mark - Errors + +- (IBAction)openErrorsPanel:(id)aSender +{ + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + [self.errorsPanel makeKeyAndOrderFront:nil]; +} + +- (IBAction)openErrorInEditor:(id)sender +{ + id info = self.errorListController.selection; + + NSString *path = [info valueForKey:@"path"]; + + if (path == NSNoSelectionMarker) + return; + + if ([self isObjjFile:path]) + { + [self openObjjFile:path line:[[info valueForKey:@"line"] intValue]]; + } + else // xib + { + [[NSWorkspace sharedWorkspace] openFile:path]; + } +} + +- (void)openObjjFile:(NSString *)path line:(NSInteger)line +{ + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; + + NSString *app, *type; + BOOL success = [workspace getInfoForFile:path application:&app type:&type]; + + if (!success) + { + NSBeep(); + return; + } + + NSBundle *bundle = [NSBundle bundleWithPath:app]; + NSString *identifier = bundle.bundleIdentifier; + NSString *executablePath = nil; + XCCLineSpecifier lineSpecifier = kLineSpecifierNone; + + if ([identifier hasPrefix:@"com.sublimetext."]) + { + lineSpecifier = kLineSpecifierColon; + executablePath = [[bundle sharedSupportPath] stringByAppendingPathComponent:@"bin/subl"]; + } + else if ([identifier isEqualToString:@"com.barebones.textwrangler"]) + { + lineSpecifier = kLineSpecifierColon; + executablePath = [[bundle bundlePath] stringByAppendingPathComponent:@"Contents/Helpers/edit"]; + } + else if ([identifier isEqualToString:@"com.barebones.bbedit"]) + { + lineSpecifier = kLineSpecifierColon; + executablePath = [[bundle bundlePath] stringByAppendingPathComponent:@"Contents/Helpers/bbedit"]; + } + else if ([identifier isEqualToString:@"com.macromates.textmate"]) // TextMate 1.x + { + lineSpecifier = kLineSpecifierMinusL; + executablePath = [[bundle sharedSupportPath] stringByAppendingPathComponent:@"Support/bin/mate"]; + } + else if ([identifier hasPrefix:@"com.macromates.TextMate"]) // TextMate 2.x + { + lineSpecifier = kLineSpecifierMinusL; + executablePath = [bundle pathForResource:@"mate" ofType:@""]; + } + else if ([identifier isEqualToString:@"com.chocolatapp.Chocolat"]) + { + lineSpecifier = kLineSpecifierMinusL; + executablePath = [[bundle sharedSupportPath] stringByAppendingPathComponent:@"choc"]; + } + else if ([identifier isEqualToString:@"org.vim.MacVim"]) + { + lineSpecifier = kLineSpecifierPlus; + executablePath = @"/usr/local/bin/mvim"; + } + else if ([identifier isEqualToString:@"org.gnu.Aquamacs"]) + { + if ([self.fm isExecutableFileAtPath:@"/usr/bin/aquamacs"]) + executablePath = @"/usr/bin/aquamacs"; + else if ([self.fm isExecutableFileAtPath:@"/usr/local/bin/aquamacs"]) + executablePath = @"/usr/local/bin/aquamacs"; + } + else if ([identifier isEqualToString:@"com.apple.dt.Xcode"]) + { + executablePath = [[bundle bundlePath] stringByAppendingPathComponent:@"Contents/Developer/usr/bin/xed"]; + } + + if (!executablePath || ![self.fm isExecutableFileAtPath:executablePath]) + { + [workspace openFile:path]; + return; + } + + NSArray *args; + + switch (lineSpecifier) + { + case kLineSpecifierNone: + args = @[path]; + break; + + case kLineSpecifierColon: + args = @[[NSString stringWithFormat:@"%1$@:%2$ld", path, line]]; + break; + + case kLineSpecifierMinusL: + args = @[@"-l", [NSString stringWithFormat:@"%ld", line], path]; + break; + + case kLineSpecifierPlus: + args = @[[NSString stringWithFormat:@"+%ld", line], path]; + break; + } + + [self runTaskWithLaunchPath:executablePath arguments:args returnType:kTaskReturnTypeNone]; +} + +- (void)showErrors +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + + if (([defaults boolForKey:kDefaultXCCAutoOpenErrorsPanelOnErrors] && self.hasErrors) || + ([defaults boolForKey:kDefaultXCCAutoOpenErrorsPanelOnWarnings] && self.errorList.count)) + { + [self openErrorsPanel:self]; + } +} + +- (void)pruneProcessingErrorsForProjectPath:(NSString *)path +{ + // Remove all errors for the path being processed + NSIndexSet *matchingErrors = [self.errorList indexesOfObjectsPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) + { + return [[obj valueForKey:@"path"] isEqualToString:path]; + }]; + + [self.errorListController removeObjectsAtArrangedObjectIndexes:matchingErrors]; +} + +- (IBAction)clearErrors:(id)sender +{ + [self.errorList removeAllObjects]; + self.errorListController.content = self.errorList; +} + +- (BOOL)hasErrors +{ + NSInteger index = [self.errorList indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) + { + return [[obj valueForKey:@"status"] intValue] == XCCStatusCodeError; + }]; + + return index != NSNotFound; +} + +#pragma mark - User notifications + +- (NSString *)applicationNameForGrowl +{ + return @"XcodeCapp"; +} + +- (void)notifyUserWithTitle:(NSString *)aTitle message:(NSString *)aMessage +{ + if ([NSUserNotificationCenter class]) + { + NSUserNotification *note = [NSUserNotification new]; + note.title = aTitle; + note.informativeText = aMessage; + + [[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:note]; + } + else + { + [GrowlApplicationBridge notifyWithTitle:aTitle + description:aMessage + notificationName:GROWL_NOTIFICATIONS_DEFAULT + iconData:nil + priority:0 + isSticky:NO + clickContext:nil]; + } +} + +- (void)wantUserNotificationWithInfo:(NSDictionary *)info +{ + [self performSelectorOnMainThread:@selector(notifyUserWithInfo:) withObject:info waitUntilDone:NO]; +} + +- (void)notifyUserWithInfo:(NSDictionary *)info +{ + if ([info[@"projectId"] intValue] != self.projectId) + return; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:kDefaultShowProcessingNotices]) + [self notifyUserWithTitle:info[@"title"] message:info[@"message"]]; +} + +- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification +{ + // Notification Center may decide not to show a notification. We always want them to show. + return YES; +} + +@end + + +@implementation XcodeCapp (SnowLeopard) + +- (void)updateLastModificationDate:(NSDate *)date forPath:(NSString *)path +{ + if (date) + [self.pathModificationDates setObject:date forKey:path]; + else + [self.pathModificationDates removeObjectForKey:path]; + + [[NSUserDefaults standardUserDefaults] setObject:self.pathModificationDates forKey:kDefaultPathModificationDates]; +} + +- (NSDate *)lastModificationDateForPath:(NSString *)path +{ + if (!self.pathModificationDates) + { + self.pathModificationDates = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:kDefaultPathModificationDates] mutableCopy]; + + if (!self.pathModificationDates) + self.pathModificationDates = [NSMutableDictionary new]; + } + + if ([self.pathModificationDates valueForKey:path] != nil) + return [self.pathModificationDates valueForKey:path]; + else + return self.appStartedTimestamp; +} + +@end diff --git a/Tools/XcodeCapp/XcodeCapp/en.lproj/MainMenu.xib b/Tools/XcodeCapp/XcodeCapp/en.lproj/MainMenu.xib index 448a6bc06..86070f198 100644 --- a/Tools/XcodeCapp/XcodeCapp/en.lproj/MainMenu.xib +++ b/Tools/XcodeCapp/XcodeCapp/en.lproj/MainMenu.xib @@ -6,12 +6,13 @@ 3084 1187.37 626.00 - - com.apple.InterfaceBuilder.CocoaPlugin - 3084 - + + 3084 + 2053 + NSArrayController + NSBox NSButton NSButtonCell NSCustomObject @@ -27,13 +28,14 @@ NSTableView NSTextField NSTextFieldCell - NSTextView NSUserDefaultsController NSView NSWindowTemplate + PDFView com.apple.InterfaceBuilder.CocoaPlugin + com.apple.pdfkit.ibplugin PluginDependencyRecalculationVersion @@ -84,6 +86,7 @@ 2147483647 + 1 @@ -171,9 +174,9 @@ AppController - 19 + 3 2 - {{196, 240}, {358, 185}} + {{196, 240}, {402, 202}} 1685586944 About NSPanel @@ -186,23 +189,24 @@ 268 - {{164, 23}, {172, 98}} + {{164, 20}, {203, 98}} + YES 68157504 4326400 - WGNvZGVDYXBwIGRldmVsb3BlZCBieToKCiAgIEFudG9pbmUgTWVyY2FkYWwKICAgYW50b2luZS5tZXJj -YWRhbEBnbWFpbC5jb20KCiAgIEFwYXJhaml0YSBGaXNobWFuCiAgIGFwYXJhaml0YUBhcGFyYWppdGEu -Y29tA + WGNvZGVDYXBwIDMgZGV2ZWxvcGVkIGJ5OgogICAgQXBhcmFqaXRhIEZpc2htYW4KICAgIGFwYXJhaml0 +YUBhcGFyYWppdGEuY29tCgpPcmlnaW5hbCBDb2NvYSB2ZXJzaW9uIGRldmVsb3BlZCBieToKICAgIEFu +dG9pbmUgTWVyY2FkYWwKICAgIGFudG9pbmUubWVyY2FkYWxAZ21haWwuY29tA LucidaGrande 11 3100 - + 6 System textBackgroundColor @@ -211,14 +215,9 @@ Y29tA MQA - - 6 - System - textColor - - 3 - MAA - + + 3 + MC4zMTM0MzA2NDI2AA NO @@ -226,18 +225,19 @@ Y29tA 266 - {{164, 140}, {160, 14}} + {{164, 126}, {221, 74}} + YES 68157504 4326400 - version + XcodeCapp 3.0.0 - LucidaGrande-Bold - 11 - 3357 + Zapfino + 18 + 16 @@ -249,7 +249,15 @@ Y29tA MC42NjY2NjY2NjY3AA - + + 6 + System + textColor + + 3 + MAA + + NO @@ -264,8 +272,9 @@ Y29tA NeXT Encapsulated PostScript v1.2 pasteboard type NeXT TIFF v4.0 pasteboard type - {{17, 46}, {128, 128}} + {{17, 63}, {128, 128}} + _NS:9 YES @@ -286,8 +295,9 @@ Y29tA YES - {358, 185} + {402, 202} + {{0, 0}, {2560, 1418}} @@ -295,9 +305,9 @@ Y29tA NO - 31 + 15 2 - {{462, 324}, {550, 237}} + {{1826, 825}, {550, 237}} 1685586944 Errors & Warnings NSPanel @@ -305,7 +315,7 @@ Y29tA {315, 77} - + 256 @@ -322,6 +332,7 @@ Y29tA {550, 201} + YES NO @@ -399,6 +410,7 @@ Y29tA {{1, 1}, {550, 201}} + @@ -409,6 +421,7 @@ Y29tA -2147483392 {{535, 1}, {16, 45}} + NO @@ -420,6 +433,7 @@ Y29tA -2147483392 {{1, 273}, {512, 16}} + NO 1 @@ -430,6 +444,7 @@ Y29tA {{-1, 35}, {552, 203}} + 133682 @@ -445,6 +460,7 @@ Y29tA 289 {{352, 8}, {85, 19}} + YES @@ -471,6 +487,7 @@ Y29tA 292 {{20, 8}, {85, 19}} + YES @@ -493,6 +510,7 @@ Y29tA 289 {{445, 8}, {85, 19}} + YES -2080374784 @@ -511,10 +529,12 @@ Y29tA {550, 237} + + {{0, 0}, {2560, 1418}} - {315, 93} + {315, 99} {10000000000000, 10000000000000} errorPanel YES @@ -522,7 +542,7 @@ Y29tA 3 2 - {{529, 541}, {341, 155}} + {{1964, 505}, {431, 292}} 1685586944 Preferences NSWindow @@ -532,208 +552,336 @@ Y29tA 256 - + - 268 - {{18, 119}, {295, 18}} - - - - YES - - -2080374784 - 0 - Listen to the most recent project on launch - - LucidaGrande - 13 - 1044 - - - 1211912448 - 2 - - NSImage - NSSwitch - - - NSSwitch - - - - 200 - 25 - - NO - - - - 268 - {{37, 58}, {81, 18}} - - - - YES - - -2080374784 - 0 - Warnings - - - 1211912448 - 2 - - - - - 200 - 25 - - NO - - - - 268 - {{139, 58}, {60, 18}} - - - - YES - - -2080374784 - 0 - Errors - - - 1211912448 - 2 - - - - - 200 - 25 - - NO - - - - 268 - {{17, 21}, {106, 17}} - - - - _NS:1535 - YES - - 68157504 - 272630784 - Recent projects: - - _NS:1535 - - - - - NO - - - - 268 - {{126, 15}, {56, 26}} - - - - _NS:9 - YES - - -2076180416 - 2048 - - _NS:9 - - 109199360 - 129 - - - 400 - 75 - - - 30 - - 1048576 - 2147483647 - 1 - - - _popUpItemAction: - - - YES - - OtherViews - - - - 10 - - 1048576 - 2147483647 - - - _popUpItemAction: - + 12 + + + + 274 + + + + 268 + {{16, 81}, {271, 18}} + + + + YES + + -2080374784 + 0 + Load the most recent project on launch + + LucidaGrande + 13 + 1044 + + + 1211912448 + 2 + + NSImage + NSSwitch + + + NSSwitch + + + + 200 + 25 + + NO - - - 20 - - 1048576 - 2147483647 - - - _popUpItemAction: - + + + 268 + {{16, 51}, {241, 18}} + + + + YES + + -2080374784 + 0 + Automatically open Xcode projects + + + 1211912448 + 2 + + + + + 200 + 25 + + NO + + + + 268 + {{15, 20}, {106, 17}} + + + + _NS:1535 + YES + + 68157504 + 272630784 + Recent projects: + + _NS:1535 + + + + + NO + + + + 268 + {{124, 14}, {56, 26}} + + + + _NS:9 + YES + + -2076180416 + 2048 + + _NS:9 + + 109199360 + 129 + + + 400 + 75 + + + 30 + + 1048576 + 2147483647 + 1 + + + _popUpItemAction: + + + YES + + OtherViews + + + + 10 + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + 20 + + 1048576 + 2147483647 + + + _popUpItemAction: + + + + + + + 2 + 1 + YES + YES + 2 + + NO - - + {{1, 1}, {395, 112}} + + + + _NS:11 - 2 - 1 - YES - YES - 2 - - NO - - - - 268 - {{18, 84}, {306, 17}} + + {{17, 149}, {397, 128}} - - _NS:1535 - YES - - 68157504 - 272630784 - Automatically open Errors & Warnings panel on: - - _NS:1535 - - - + + _NS:9 + {0, 0} + + 67108864 + 0 + Projects + + + + 3 + MCAwLjgwMDAwMDAxMTkAA + - NO + + 1 + 0 + 2 + NO + + + + 12 + + + + 274 + + + + 268 + {{16, 73}, {363, 18}} + + + + YES + + -2080374784 + 0 + Show notifications when individual files are processed + + + 1211912448 + 2 + + + + + 200 + 25 + + NO + + + + 268 + {{35, 17}, {81, 18}} + + + + YES + + -2080374784 + 0 + Warnings + + + 1211912448 + 2 + + + + + 200 + 25 + + NO + + + + 268 + {{137, 17}, {60, 18}} + + + + YES + + -2080374784 + 0 + Errors + + + 1211912448 + 2 + + + + + 200 + 25 + + NO + + + + 268 + {{16, 43}, {306, 17}} + + + + _NS:1535 + YES + + 68157504 + 272630784 + Automatically open Errors & Warnings panel on: + + _NS:1535 + + + + + NO + + + {{1, 1}, {395, 104}} + + + + _NS:11 + + + {{17, 16}, {397, 120}} + + + + _NS:9 + {0, 0} + + 67108864 + 0 + File processing + + + + 3 + MCAwLjgwMDAwMDAxMTkAA + + + + 1 + 0 + 2 + NO - {341, 155} + {431, 292} - + {{0, 0}, {2560, 1418}} {10000000000000, 10000000000000} @@ -743,158 +891,38 @@ Y29tA 15 2 - {{163, 199}, {716, 571}} + {{163, 199}, {716, 654}} 1685586944 - Help + XcodeCapp Help NSWindow - + 256 - + - 274 - - - - 2304 - - - - 2322 - {716, 571} - - - - - - - - - - - - - - 166 - - - - 716 - 1 - - - 67120389 - 0 - - - - - 6 - System - selectedTextBackgroundColor - - - - 6 - System - selectedTextColor - - - - - - - 1 - MCAwIDEAA - - - {8, -8} - 13 - - - - - - 1 - - 6 - {716, 10000000} - - - - {{1, 1}, {716, 571}} - - - - - - {4, 5} - - 12582912 - - - - - - TU0AKgAAAHCAFUqgBVKsAAAAwdVQUqwaEQeIRGJRGFlYqwWLQ+JxuOQpVRmEx2RROKwOQyOUQSPyaUym -SxqWyKXyeYxyZzWbSuJTScRCbz2Nz+gRKhUOfTqeUai0OSxiWTiBQSHSGFquGwekxyAgAAAOAQAAAwAA -AAEAEAAAAQEAAwAAAAEAEAAAAQIAAwAAAAIACAAIAQMAAwAAAAEABQAAAQYAAwAAAAEAAQAAAREABAAA -AAEAAAAIARIAAwAAAAEAAQAAARUAAwAAAAEAAgAAARYAAwAAAAEAEAAAARcABAAAAAEAAABnARwAAwAA -AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA - - - - - - 3 - MCAwAA - - - - 4 - - - - 256 - {{701, 1}, {16, 571}} - - NO - - _doScroller: - 1 - 0.85256409645080566 - - - - -2147483392 - {{-100, -100}, {87, 18}} - - - NO - 1 - - _doScroller: - 1 - 0.94565218687057495 - - - {{-1, -1}, {718, 573}} + 18 + + + NSFilenamesPboardType + + {716, 654} - - 133138 - - - - 0.25 - 4 - 1 + + + _NS:9 + 1 + NO + 1.1454248428344727 + YES - {716, 571} - + {716, 654} + + + {{0, 0}, {2560, 1418}} {10000000000000, 10000000000000} @@ -904,7 +932,7 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA YES - TNXcodeCapp + XcodeCapp @@ -952,14 +980,6 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA 610 - - - helpTextView - - - - 611 - openHelp: @@ -1000,14 +1020,6 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA 617 - - - listenToProject: - - - - 619 - menuItemHistory @@ -1048,6 +1060,22 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA 796 + + + loadProject: + + + + 832 + + + + helpView + + + + 858 + performClose: @@ -1064,6 +1092,14 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA 634 + + + save: + + + + 841 + save: @@ -1080,6 +1116,22 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA 821 + + + save: + + + + 849 + + + + save: + + + + 854 + errorTable @@ -1136,14 +1188,6 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA 786 - - - openInXcode: - - - - 815 - resetProject: @@ -1152,6 +1196,14 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA 816 + + + openProjectInXcode: + + + + 831 + value: values.XCCReopenLastProject @@ -1267,19 +1319,60 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA - enabled: currentProjectPath.length + enabled: isLoadingProject + + + + + + enabled: isLoadingProject + enabled + isLoadingProject + 2 + + + 835 + + + + enabled: projectPath.length + + + + + + enabled: projectPath.length + enabled + projectPath.length + + 2 + + + 836 + + + + enabled2: isProcessing - enabled: currentProjectPath.length - enabled - currentProjectPath.length + enabled2: isProcessing + enabled2 + isProcessing + + + + + + NSNegateBoolean + + 2 - 624 + 837 @@ -1351,15 +1444,15 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA - enabled: currentProjectPath.length + enabled: projectPath.length - enabled: currentProjectPath.length + enabled: projectPath.length enabled - currentProjectPath.length + projectPath.length 2 @@ -1385,6 +1478,46 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA 824 + + + value: values.autoOpenXcodeProject + + + + + + value: values.autoOpenXcodeProject + value + values.autoOpenXcodeProject + + NSValidatesImmediately + + + 2 + + + 851 + + + + value: values.showProcessingNotices + + + + + + value: values.showProcessingNotices + value + values.showProcessingNotices + + NSValidatesImmediately + + + 2 + + + 856 + @@ -1488,61 +1621,19 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA 553 - + - - 554 - - - - - - - - - - 555 - - - - - 556 - - - - - 557 - - - 558 - - - - - - + + - - 563 - - - - - - - - 576 - - - 581 @@ -1731,19 +1822,6 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA - - 789 - - - - - - - - 790 - - - 797 @@ -1758,17 +1836,83 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA - 801 - + 811 + + + + + 848 + - + + + + - 802 - - + 843 + + + + + + + + 844 + + + + + 789 + + + + + + + + 790 + + + + + 819 + + + + + + + + 820 + + + + + 817 + + + + + + + + 818 + + + + + 847 + + + + + + + + 803 @@ -1776,7 +1920,7 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA - + 804 @@ -1790,9 +1934,9 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA 805 - - + + @@ -1812,35 +1956,48 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA - 811 - - - - - 817 - + 801 + - + - + - 818 - - + 802 + + - 819 - + 839 + - + - + - 820 - - + 840 + + + + + 563 + + + + + + + + 576 + + + + + 857 + + @@ -1856,22 +2013,21 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin + + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -1886,6 +2042,7 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -1937,12 +2094,35 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin + + ToolTip + + ToolTip + + If this is checked, when a Cappuccino project is opened, the project’s Xcode support project will be opened automatically + + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + ToolTip + + ToolTip + + If this is checked, when a Cappuccino project is opened, the project’s Xcode support project will be opened automatically + + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.pdfkit.ibplugin - 824 + 858 @@ -1950,14 +2130,14 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA AppController NSObject - id + id id id id - - listenToProject: + + loadProject: id @@ -1975,7 +2155,7 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA NSPanel - NSTextView + PDFView NSWindow NSMenuItem NSMenuItem @@ -1983,16 +2163,16 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA NSUserDefaultsController NSWindow NSMenu - TNXcodeCapp + XcodeCapp aboutWindow NSPanel - - helpTextView - NSTextView + + helpView + PDFView helpWindow @@ -2024,7 +2204,7 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA xcc - TNXcodeCapp + XcodeCapp @@ -2033,34 +2213,13 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA - NSTextView - - id - id - - - - orderFrontSharingServicePicker: - id - - - toggleQuickLookPreviewPanel: - id - - - - IBProjectSource - ./Classes/NSTextView.h - - - - TNXcodeCapp + XcodeCapp NSObject id id id - id + id id @@ -2076,8 +2235,8 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA openErrorsPanel: id - - openInXcode: + + openProjectInXcode: id @@ -2106,7 +2265,7 @@ AAEAAQAAAT0AAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAIAAQABAAAAAA IBProjectSource - ./Classes/TNXcodeCapp.h + ./Classes/XcodeCapp.h diff --git a/Tools/XcodeCapp/XcodeCapp/macros.h b/Tools/XcodeCapp/XcodeCapp/macros.h deleted file mode 100644 index 7e337dfd6..000000000 --- a/Tools/XcodeCapp/XcodeCapp/macros.h +++ /dev/null @@ -1,28 +0,0 @@ -/* - * This file is a part of program XcodeCapp - * Copyright (C) 2011 Aparajita Fishman () - * - * 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 . - */ - -#ifndef xcodecapp_cocoa_macros_h -#define xcodecapp_cocoa_macros_h - -#if DEBUG -# define DLog(fmt, ...) NSLog((fmt), ##__VA_ARGS__) -#else -# define DLog(...) -#endif - -#endif