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

Last change on this file since 256 was 256, checked in by Nicholas Riley, 14 years ago

Removed TextEdit? support; merged new Unsanity Installer (3.6.1) and Application Enhancer (2.0.1).

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