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

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

Operate on the link target, rather than the text, of NSTextView hyperlinks

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