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

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

Support WebKit? past r31014; patch PDFView in some places (not WebKit? yet).

File size: 13.7 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            if (ICCF_PatchMethod("XCTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:")) {
131                ICCF_PatchMethod("XCSourceCodeTextView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
132                ICCF_PatchMethod("XCDiffTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:");
133                ICCF_PatchMethod("XCDiffTextView", "ICeCoffEE", "ICeCoffEESuper", "menuForEvent:"); // subclass of PBXTextView; patching both is bad
134                ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "clickedOnLink:atIndex:");
135            } else {
136                ICCF_PatchMethod("PBXTextView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
137            }
138            ICapeprintf("ICeCoffEE APE: loaded in PBXTextView / XCTextView for Xcode\n");
139            shouldLoadInNSTextView = NO;
140        } else if (ICCF_CFBundleIDMatches(bundleID, CFSTR("com.apple.terminal"))) {
141            if (ICCF_PatchMethod("TTView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:")) {
142                ICCF_PatchMethod("TTView", "ICeCoffEETTView", "ICeCoffEETTViewSuper", "mouseDown:") &&
143                ICCF_PatchMethod("TTView", "ICeCoffEETTView", "ICeCoffEETTViewSuper", "mouseUp:") &&
144                ICCF_PatchMethod("TTView", "ICeCoffEETTView", "ICeCoffEETTViewSuper", "draggingEntered:");
145                ICapeprintf("ICeCoffEE APE: loaded in TTView for Terminal\n");
146            } else {
147                ICCF_PatchMethod("TermSubview", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:") &&
148                ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "selectedRange") &&
149                ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "attributedSubstringFromRange:") &&
150                ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "mouseDown:") &&
151                ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "mouseUp:") &&
152                ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "draggingEntered:") &&
153                ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "_optionClickEvent:::");
154                ICapeprintf("ICeCoffEE APE: loaded in TermSubview for Terminal\n");
155            }
156        } else if (ICCF_CFBundleIDMatches(bundleID, CFSTR("org.mozilla.camino"))) {
157            ICCF_PatchMethod("ChildView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
158            ICapeprintf("ICeCoffEE APE: loaded in ChildView for Camino\n");
159        }
160    }
161
162    ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "mouseUp:") &&
163        ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "mouseDown:") &&
164        ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "menuForEvent:");
165    ICapeprintf("ICeCoffEE APE: loaded in WebHTMLView for WebKit/Safari 3\n");
166   
167    if (shouldLoadInNSTextView) {
168        ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:") &&
169        ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "clickedOnLink:atIndex:") &&
170        ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "menuForEvent:");
171        ICapeprintf("ICeCoffEE APE: loaded generic NSTextView support\n");
172    }
173   
174    ICCF_PatchMethod("PDFView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
175    ICapeprintf("ICeCoffEE APE: loaded in PDFView for PDFKit\n");
176
177    ICCF_ReloadPrefs();
178       
179    return;
180}
181
182// 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.
183OSStatus APEBundleMessage(CFStringRef message,CFDataRef inData,CFDataRef *outData)
184{
185    ICapeprintf("ICeCoffEE APE: message '%@' (inData = %@)\n", message, inData);
186   
187    if (CFStringCompare(message, kICPreferencesChanged, 0) == kCFCompareEqualTo)               
188    {   // request to reload prefs from our preference pane
189        ICCF_ReloadPrefs();
190    }
191   
192    return noErr;
193}
194
195Boolean ICCF_GetBooleanPref(CFStringRef prefKey, Boolean defaultValue) {
196    Boolean keyExists;
197    Boolean value = CFPreferencesGetAppBooleanValue(prefKey, kICBundleIdentifier, &keyExists);
198    if (keyExists) return value;
199    CFPreferencesSetAppValue(prefKey, defaultValue ? kCFBooleanTrue : kCFBooleanFalse, kICBundleIdentifier);
200    return defaultValue;
201}
202
203CFIndex ICCF_GetCFIndexPref(CFStringRef prefKey, CFIndex defaultValue) {
204    Boolean keyExists;
205    CFIndex value = CFPreferencesGetAppIntegerValue(prefKey, kICBundleIdentifier, &keyExists);
206    if (keyExists) return value;
207    CFNumberRef defaultValueNumber = CFNumberCreate(NULL, kCFNumberCFIndexType, &defaultValue);
208    CFPreferencesSetAppValue(prefKey, defaultValueNumber, kICBundleIdentifier);
209    CFRelease(defaultValueNumber);
210    return defaultValue;
211}
212
213void ICCF_GetCFTypePref(CFStringRef prefKey, CFTypeRef *val, CFTypeID type) {
214    if (*val != NULL) {
215        CFRelease(*val);
216        *val = NULL;
217    }
218   
219    *val = CFPreferencesCopyAppValue(prefKey, kICBundleIdentifier);
220
221    if (*val == NULL) return;
222
223    if (CFGetTypeID(*val) != type) {
224        CFRelease(*val);
225        *val = NULL;
226        return;
227    }
228}
229
230// to test: defaults write net.sabi.ICeCoffEE "Excluded Applications" -array '{CFBundleID = "com.apple.projectbuilder"; }' '{CFBundleID = "net.sabi.Pester"; }' '{CFBundleID = "com.apple.foobar"; }'
231static void ICCF_MigratePrefs() {
232   
233    CFArrayRef prefExcludedApps = CFPreferencesCopyAppValue(kIC12PrefExcluded, kICBundleIdentifier);
234
235    if (prefExcludedApps == NULL) return;
236
237    if (CFGetTypeID(prefExcludedApps) != CFArrayGetTypeID()) {
238        CFRelease(prefExcludedApps);
239        return;
240    }
241   
242    CFMutableArrayRef excludedApps = CFArrayCreateMutableCopy(NULL, 0, prefExcludedApps);
243    CFRelease(prefExcludedApps); prefExcludedApps = NULL;
244
245    ICapeprintf("Excluded apps: %@\n", excludedApps);
246
247    CFIndex excludedAppCount = CFArrayGetCount(excludedApps);
248    CFIndex excludedAppIndex;
249
250    CFDictionaryRef excludedAppSpecifiers = NULL;
251    CFStringRef excludedAppBundleID = NULL;
252    CFURLRef excludedAppURL = NULL;
253    CFBundleRef excludedAppBundle = NULL;
254
255    BOOL postponeMigrationForApp;
256
257    for (excludedAppIndex = excludedAppCount - 1 ; excludedAppIndex >= 0 ; excludedAppIndex--) {
258        if ( (excludedAppSpecifiers = CFArrayGetValueAtIndex(excludedApps, excludedAppIndex)) == NULL || CFGetTypeID(excludedAppSpecifiers) != CFDictionaryGetTypeID()) {
259            CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
260            continue;
261        }
262
263        postponeMigrationForApp = NO;
264        if ( (excludedAppBundleID = CFDictionaryGetValue(excludedAppSpecifiers, kIC12PrefExcludedAppSpecifierBundleID)) != NULL
265             && CFGetTypeID(excludedAppBundleID) == CFStringGetTypeID()
266             && CFStringCompare(excludedAppBundleID, CFSTR("com.apple.projectbuilder"), kCFCompareCaseInsensitive) != kCFCompareEqualTo) {
267            if (LSFindApplicationForInfo(kLSUnknownCreator, excludedAppBundleID, NULL, NULL, &excludedAppURL) == noErr) {
268                excludedAppBundle = CFBundleCreate(NULL, excludedAppURL);
269                if (excludedAppBundle != NULL) {
270                    APEToolsAddToExcludeList(kICBundleIdentifier, excludedAppBundle, CFSTR("Migrated from ICeCoffEE 1.0-1.2"), NO);
271                } else postponeMigrationForApp = YES; // can't create bundle
272            } else postponeMigrationForApp = YES; // can't find app
273        }
274        // don't release excludedAppSpecifiers, used Get
275        // don't release excludedAppBundleID, used Get
276        if (excludedAppURL != NULL) { CFRelease(excludedAppURL); excludedAppURL = NULL; }
277        if (excludedAppBundle != NULL) { CFRelease(excludedAppBundle); excludedAppBundle = NULL; }
278        if (!postponeMigrationForApp) CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
279    }
280
281    ICapeprintf("Excluded apps remaining: %@\n", excludedApps);
282    CFPreferencesSetAppValue(kIC12PrefExcluded, (CFArrayGetCount(excludedApps) == 0 ? NULL : excludedApps), kICBundleIdentifier);
283    CFRelease(excludedApps);
284
285    CFPreferencesAppSynchronize(kICBundleIdentifier);
286}
287
288static void ICCF_ReloadPrefs() {
289    CFPreferencesAppSynchronize(kICBundleIdentifier);
290
291    ICCF_prefs.commandClickEnabled = ICCF_GetBooleanPref(kICCommandClickEnabled, YES);
292    ICCF_prefs.textBlinkEnabled = ICCF_GetBooleanPref(kICTextBlinkEnabled, YES);
293    ICCF_prefs.textBlinkCount = ICCF_GetCFIndexPref(kICTextBlinkCount, 3);
294    ICCF_prefs.servicesInContextualMenu = ICCF_GetBooleanPref(kICServicesInContextualMenu, YES);
295    ICCF_prefs.servicesInMenuBar = ICCF_GetBooleanPref(kICServicesInMenuBar, NO);
296    ICCF_GetCFTypePref(kICServiceOptions, (CFTypeRef *)&ICCF_prefs.serviceOptions, CFDictionaryGetTypeID());
297    ICCF_prefs.terminalRequireOptionForSelfDrag = ICCF_GetBooleanPref(kICTerminalRequireOptionForSelfDrag, NO);
298    ICCF_prefs.errorSoundEnabled = ICCF_GetBooleanPref(kICErrorSoundEnabled, NO);
299    ICCF_prefs.errorDialogEnabled = ICCF_GetBooleanPref(kICErrorDialogEnabled, YES);
300
301    CFPreferencesAppSynchronize(kICBundleIdentifier);
302
303    ICCF_AddRemoveServicesMenu();
304}
Note: See TracBrowser for help on using the repository browser.