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

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

Services in PDFView/WebPDFView; activate on dynamic WebKit?/PDFKit loading.

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