source: trunk/ICeCoffEE/ICeCoffEE/APEMain.m @ 391

Last change on this file since 391 was 391, checked in by Nicholas Riley, 12 years ago

APEMain.m: Xcode 3 URL launching support

File size: 13.1 KB
Line 
1// ===========================================================================
2//
3//      File:           APEMain.m
4//
5//      Contains:       ICeCoffEE APE Module code
6//
7//      Copyright:      Copyright (c) 2003, Nicholas Riley
8//                      All Rights Reserved.
9//
10//      Author(s):      Nicholas Riley (Sun Jan 19 2003)
11//
12// ===========================================================================
13
14#import <Carbon/Carbon.h>
15#import <ApplicationEnhancer/ApplicationEnhancer.h>
16#import <ApplicationEnhancer/APETools.h>
17#import <objc/objc-runtime.h>
18#import "ICeCoffEE.h"
19
20//¥¥¥ Our settings
21
22//¥¥¥ Function prototypes
23static void ICCF_ReloadPrefs();                         // reloads our preferences
24static void ICCF_MigratePrefs();                        // migrates prefs from 1.0Ð1.2
25
26//¥¥¥ Enter sandman, the code begins --
27
28#define ICCF_GET_PATCHCLASS(patchclass) \
29    struct objc_class *patchclass = objc_getClass(patchclass ## Name); \
30    if (patchclass == NULL) { \
31        ICapeprintf("can't get %s\n", patchclass ## Name); \
32        return NO; \
33    }
34
35#define ICCF_GET_METHOD(name, patchclass, sel) \
36    Method name = class_getInstanceMethod(patchclass, sel); \
37    if (name == NULL) { \
38        ICapeprintf("can't get %s\n", patchclass ## Name); \
39        return NO; \
40    }
41
42BOOL ICCF_PatchMethod(char *patcheeClassName,
43                      char *patchClassName,
44                      char *patchSuperclassName,
45                      char *selectorString) {
46
47    ICCF_GET_PATCHCLASS(patcheeClass);
48    ICCF_GET_PATCHCLASS(patchClass);
49    ICCF_GET_PATCHCLASS(patchSuperclass);
50
51    SEL selector = sel_getUid(selectorString);
52
53    ICCF_GET_METHOD(patcheeMethod, patcheeClass, selector);
54    ICCF_GET_METHOD(patchMethod, patchClass, selector);
55    ICCF_GET_METHOD(patchSuperMethod, patchSuperclass, selector);
56
57    if (APEPatchCreate(patchSuperMethod->method_imp,
58                       APEPatchCreate(patcheeMethod->method_imp, patchMethod->method_imp)) == NULL) {
59        ICapeprintf("can't patch class %s with [%s %s] super %s", patcheeClassName, patchClassName, selectorString, patchSuperclassName);
60        return NO;
61    }
62    return YES;
63}
64
65CFBundleRef ICCF_bundle;
66
67// With APE 1.3, if we're in the exclude list, APEBundleMainEarlyLoad doesn't get invoked; don't need to use APETools.  But we need to do our own management to avoid loading in background-only applications.
68Boolean ICCF_shouldLoad;
69
70static Boolean ICCF_IsOne(CFTypeRef value) {
71    if (value == NULL)
72        return false;
73    CFTypeID typeID = CFGetTypeID(value);
74    if (typeID == CFBooleanGetTypeID())
75        return CFBooleanGetValue((CFBooleanRef)value);
76    else if (typeID == CFNumberGetTypeID()) {
77        static const int one = 1;
78        static CFNumberRef oneRef = NULL;
79        if (oneRef == NULL) oneRef = CFNumberCreate(NULL, kCFNumberIntType, &one);
80        return CFNumberCompare((CFNumberRef)value, oneRef, NULL) == kCFCompareEqualTo;
81    } else if (typeID == CFStringGetTypeID()) {
82        return CFStringCompare((CFStringRef)value, CFSTR("1"), 0) == kCFCompareEqualTo;
83    }
84    return false;
85}
86
87void APEBundleMainEarlyLoad(CFBundleRef inBundle, CFStringRef inAPEToolsApplicationID)
88{
89    ICCF_MigratePrefs();
90
91    UInt32 icVersion = CFBundleGetVersionNumber(inBundle);
92    ICapeprintf("ICeCoffEE APE: bundle version is %ld (0x%x)\n", icVersion, icVersion);
93    CFNumberRef icVersionRef = CFNumberCreate(NULL, kCFNumberLongType, &icVersion);
94    CFPreferencesSetAppValue(kICLastLoadedVersion, icVersionRef, kICBundleIdentifier);
95    SAFE_RELEASE(icVersionRef);
96
97    ICCF_shouldLoad = false;
98   
99    CFBundleRef appBundle = CFBundleGetMainBundle();
100    if (appBundle == NULL) {
101        apeprintf("ICeCoffEE APE: Can't get CFBundle for current process; not loading\n");
102        return;
103    }
104   
105    if (ICCF_IsOne(CFBundleGetValueForInfoDictionaryKey(appBundle, CFSTR("LSUIElement"))) ||
106        ICCF_IsOne(CFBundleGetValueForInfoDictionaryKey(appBundle, CFSTR("NSUIElement"))) ||
107        ICCF_IsOne(CFBundleGetValueForInfoDictionaryKey(appBundle, CFSTR("LSBackgroundOnly"))) ||
108        ICCF_IsOne(CFBundleGetValueForInfoDictionaryKey(appBundle, CFSTR("NSBackgroundOnly")))) {
109        ICapeprintf("ICeCoffEE APE: not loading as this application is background-only\n");
110        return;
111    }
112    ICCF_shouldLoad = true;
113}
114
115static Boolean ICCF_CFBundleIDMatches(CFStringRef bundleID, CFStringRef test) {
116    return CFStringCompare(bundleID, test, kCFCompareCaseInsensitive) == kCFCompareEqualTo;
117}
118
119void APEBundleMainLateLoad(CFBundleRef inBundle, CFStringRef inAPEToolsApplicationID)
120{
121    if (!ICCF_shouldLoad) return;
122
123    ICCF_bundle = inBundle;
124    CFStringRef bundleID = CFBundleGetIdentifier(CFBundleGetMainBundle());
125    BOOL shouldLoadInNSTextView = YES;
126
127    // XXX handle patching error return from ICCF_PatchMethod
128    if (bundleID != NULL) {
129        if (ICCF_CFBundleIDMatches(bundleID, CFSTR("com.apple.xcode"))) {
130            ICCF_PatchMethod("PBXTextView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
131            // XXX XCTextView doesn't use menuForEvent:
132            ICCF_PatchMethod("XCTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:");
133            ICapeprintf("ICeCoffEE APE: loaded in PBXTextView / XCTextView for Xcode\n");
134            shouldLoadInNSTextView = NO;
135        } else if (ICCF_CFBundleIDMatches(bundleID, CFSTR("com.apple.terminal"))) {
136            if (ICCF_PatchMethod("TTView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:")) {
137                ICCF_PatchMethod("TTView", "ICeCoffEETTView", "ICeCoffEETTViewSuper", "mouseDown:") &&
138                ICCF_PatchMethod("TTView", "ICeCoffEETTView", "ICeCoffEETTViewSuper", "mouseUp:") &&
139                ICCF_PatchMethod("TTView", "ICeCoffEETTView", "ICeCoffEETTViewSuper", "draggingEntered:");
140                ICapeprintf("ICeCoffEE APE: loaded in TTView for Terminal\n");
141            } else {
142                ICCF_PatchMethod("TermSubview", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:") &&
143                ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "selectedRange") &&
144                ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "attributedSubstringFromRange:") &&
145                ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "mouseDown:") &&
146                ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "mouseUp:") &&
147                ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "draggingEntered:") &&
148                ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "_optionClickEvent:::");
149                ICapeprintf("ICeCoffEE APE: loaded in TermSubview for Terminal\n");
150            }
151        } else if (ICCF_CFBundleIDMatches(bundleID, CFSTR("org.mozilla.camino"))) {
152            ICCF_PatchMethod("ChildView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
153            ICapeprintf("ICeCoffEE APE: loaded in ChildView for Camino\n");
154        }
155    }
156
157    ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "mouseUp:") &&
158        ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "mouseDown:") &&
159        ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "menuForEvent:");
160        ICapeprintf("ICeCoffEE APE: loaded in WebHTMLView for WebKit/Safari\n");
161   
162    if (shouldLoadInNSTextView) {
163        ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:") &&
164        ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "menuForEvent:");
165        ICapeprintf("ICeCoffEE APE: loaded generic NSTextView support\n");
166    }
167
168    ICCF_ReloadPrefs();
169       
170    return;
171}
172
173// We define APEBundleMessage so we can receive messages from our preference pane.  We actually only know the Refresh message that instructs our APE module to reload the preferences.  Why do we reload preferences here and not do it in our patch? Well, although you can poll CFPreferences in every patch call, that'll result in some performance loss - we better cache the settings in a static variable and refresh them from prefs only when needed.
174OSStatus APEBundleMessage(CFStringRef message,CFDataRef inData,CFDataRef *outData)
175{
176    ICapeprintf("ICeCoffEE APE: message '%@' (inData = %@)\n", message, inData);
177   
178    if (CFStringCompare(message, kICPreferencesChanged, 0) == kCFCompareEqualTo)               
179    {   // request to reload prefs from our preference pane
180        ICCF_ReloadPrefs();
181    }
182   
183    return noErr;
184}
185
186Boolean ICCF_GetBooleanPref(CFStringRef prefKey, Boolean defaultValue) {
187    Boolean keyExists;
188    Boolean value = CFPreferencesGetAppBooleanValue(prefKey, kICBundleIdentifier, &keyExists);
189    if (keyExists) return value;
190    CFPreferencesSetAppValue(prefKey, defaultValue ? kCFBooleanTrue : kCFBooleanFalse, kICBundleIdentifier);
191    return defaultValue;
192}
193
194CFIndex ICCF_GetCFIndexPref(CFStringRef prefKey, CFIndex defaultValue) {
195    Boolean keyExists;
196    CFIndex value = CFPreferencesGetAppIntegerValue(prefKey, kICBundleIdentifier, &keyExists);
197    if (keyExists) return value;
198    CFNumberRef defaultValueNumber = CFNumberCreate(NULL, kCFNumberCFIndexType, &defaultValue);
199    CFPreferencesSetAppValue(prefKey, defaultValueNumber, kICBundleIdentifier);
200    CFRelease(defaultValueNumber);
201    return defaultValue;
202}
203
204void ICCF_GetCFTypePref(CFStringRef prefKey, CFTypeRef *val, CFTypeID type) {
205    if (*val != NULL) {
206        CFRelease(*val);
207        *val = NULL;
208    }
209   
210    *val = CFPreferencesCopyAppValue(prefKey, kICBundleIdentifier);
211
212    if (*val == NULL) return;
213
214    if (CFGetTypeID(*val) != type) {
215        CFRelease(*val);
216        *val = NULL;
217        return;
218    }
219}
220
221// to test: defaults write net.sabi.ICeCoffEE "Excluded Applications" -array '{CFBundleID = "com.apple.projectbuilder"; }' '{CFBundleID = "net.sabi.Pester"; }' '{CFBundleID = "com.apple.foobar"; }'
222static void ICCF_MigratePrefs() {
223   
224    CFArrayRef prefExcludedApps = CFPreferencesCopyAppValue(kIC12PrefExcluded, kICBundleIdentifier);
225
226    if (prefExcludedApps == NULL) return;
227
228    if (CFGetTypeID(prefExcludedApps) != CFArrayGetTypeID()) {
229        CFRelease(prefExcludedApps);
230        return;
231    }
232   
233    CFMutableArrayRef excludedApps = CFArrayCreateMutableCopy(NULL, 0, prefExcludedApps);
234    CFRelease(prefExcludedApps); prefExcludedApps = NULL;
235
236    ICapeprintf("Excluded apps: %@\n", excludedApps);
237
238    CFIndex excludedAppCount = CFArrayGetCount(excludedApps);
239    CFIndex excludedAppIndex;
240
241    CFDictionaryRef excludedAppSpecifiers = NULL;
242    CFStringRef excludedAppBundleID = NULL;
243    CFURLRef excludedAppURL = NULL;
244    CFBundleRef excludedAppBundle = NULL;
245
246    BOOL postponeMigrationForApp;
247
248    for (excludedAppIndex = excludedAppCount - 1 ; excludedAppIndex >= 0 ; excludedAppIndex--) {
249        if ( (excludedAppSpecifiers = CFArrayGetValueAtIndex(excludedApps, excludedAppIndex)) == NULL || CFGetTypeID(excludedAppSpecifiers) != CFDictionaryGetTypeID()) {
250            CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
251            continue;
252        }
253
254        postponeMigrationForApp = NO;
255        if ( (excludedAppBundleID = CFDictionaryGetValue(excludedAppSpecifiers, kIC12PrefExcludedAppSpecifierBundleID)) != NULL
256             && CFGetTypeID(excludedAppBundleID) == CFStringGetTypeID()
257             && CFStringCompare(excludedAppBundleID, CFSTR("com.apple.projectbuilder"), kCFCompareCaseInsensitive) != kCFCompareEqualTo) {
258            if (LSFindApplicationForInfo(kLSUnknownCreator, excludedAppBundleID, NULL, NULL, &excludedAppURL) == noErr) {
259                excludedAppBundle = CFBundleCreate(NULL, excludedAppURL);
260                if (excludedAppBundle != NULL) {
261                    APEToolsAddToExcludeList(kICBundleIdentifier, excludedAppBundle, CFSTR("Migrated from ICeCoffEE 1.0-1.2"), NO);
262                } else postponeMigrationForApp = YES; // can't create bundle
263            } else postponeMigrationForApp = YES; // can't find app
264        }
265        // don't release excludedAppSpecifiers, used Get
266        // don't release excludedAppBundleID, used Get
267        if (excludedAppURL != NULL) { CFRelease(excludedAppURL); excludedAppURL = NULL; }
268        if (excludedAppBundle != NULL) { CFRelease(excludedAppBundle); excludedAppBundle = NULL; }
269        if (!postponeMigrationForApp) CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
270    }
271
272    ICapeprintf("Excluded apps remaining: %@\n", excludedApps);
273    CFPreferencesSetAppValue(kIC12PrefExcluded, (CFArrayGetCount(excludedApps) == 0 ? NULL : excludedApps), kICBundleIdentifier);
274    CFRelease(excludedApps);
275
276    CFPreferencesAppSynchronize(kICBundleIdentifier);
277}
278
279static void ICCF_ReloadPrefs() {
280    CFPreferencesAppSynchronize(kICBundleIdentifier);
281
282    ICCF_prefs.commandClickEnabled = ICCF_GetBooleanPref(kICCommandClickEnabled, YES);
283    ICCF_prefs.textBlinkEnabled = ICCF_GetBooleanPref(kICTextBlinkEnabled, YES);
284    ICCF_prefs.textBlinkCount = ICCF_GetCFIndexPref(kICTextBlinkCount, 3);
285    ICCF_prefs.servicesInContextualMenu = ICCF_GetBooleanPref(kICServicesInContextualMenu, YES);
286    ICCF_prefs.servicesInMenuBar = ICCF_GetBooleanPref(kICServicesInMenuBar, NO);
287    ICCF_GetCFTypePref(kICServiceOptions, (CFTypeRef *)&ICCF_prefs.serviceOptions, CFDictionaryGetTypeID());
288    ICCF_prefs.terminalRequireOptionForSelfDrag = ICCF_GetBooleanPref(kICTerminalRequireOptionForSelfDrag, NO);
289    ICCF_prefs.errorSoundEnabled = ICCF_GetBooleanPref(kICErrorSoundEnabled, NO);
290    ICCF_prefs.errorDialogEnabled = ICCF_GetBooleanPref(kICErrorDialogEnabled, YES);
291
292    CFPreferencesAppSynchronize(kICBundleIdentifier);
293
294    ICCF_AddRemoveServicesMenu();
295}
Note: See TracBrowser for help on using the repository browser.