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

Last change on this file since 167 was 167, checked in by Nicholas Riley, 15 years ago

ICeCoffEE 1.4 and preliminary 1.4.1 changes. Sorry, I forgot to
commit version 1.4 when it was released, so the precise source for
that release has been lost.

See the release notes for details of what changed in these versions.
1.4 was a significant feature release; 1.4.1 is a bug fix for 10.3.9,
incorporating up-to-date Unsanity Installer and APE.

package-ICeCoffEE.sh: use xcodebuild instead of pbxbuild.

File size: 12.8 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#import "ICeCoffEETextEdit.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 appllications.
70Boolean ICCF_shouldLoad;
71
72void APEBundleMainEarlyLoad(CFBundleRef inBundle, CFStringRef inAPEToolsApplicationID)
73{
74    ICCF_MigratePrefs();
75
76    UInt32 icVersion = CFBundleGetVersionNumber(inBundle);
77    ICapeprintf("ICeCoffEE APE: bundle version is %ld (0x%x)\n", icVersion, icVersion);
78    CFNumberRef icVersionRef = CFNumberCreate(NULL, kCFNumberLongType, &icVersion);
79    CFPreferencesSetAppValue(kICLastLoadedVersion, icVersionRef, kICBundleIdentifier);
80    SAFE_RELEASE(icVersionRef);
81
82    ICCF_shouldLoad = false;
83
84    CPSProcessSerNum psn;
85    OSStatus err = CPSGetCurrentProcess(&psn);
86    if (err != noErr) {
87#if ICCF_DEBUG
88        apeprintf("ICeCoffEE APE: Can't get process serial number for current process (error %ld); still loading because of ICCF_DEBUG\n", err);
89#else
90        apeprintf("ICeCoffEE APE: Can't get process serial number for current process (error %ld); not loading in this application\n", err);
91        return;
92#endif
93    }
94   
95    CPSProcessInfoRec info;
96    err = CPSGetProcessInfo(&psn, &info, NULL, 0, NULL, NULL, 0);
97    if (err != noErr) {
98#if ICCF_DEBUG
99        apeprintf("ICeCoffEE APE: Can't get process information (error %ld); still loading because of ICCF_DEBUG\n", err);
100#else
101        apeprintf("ICeCoffEE APE: Can't get process information (error %ld); not loading in this application\n", err);
102        return;
103#endif
104    } else {
105        ICapeprintf("ICeCoffEE APE: got process attributes = 0x%lx\n", info.Attributes);
106        if (info.Attributes & (kCPSBGOnlyAttr | kCPSUIElementAttr | kCPSFullScreenAttr)) {
107            ICapeprintf("ICeCoffEE APE: not loading as this application is background-only\n");
108            return;
109        }
110    }
111    ICCF_shouldLoad = true;
112}
113
114Boolean ICCF_CFBundleIDMatches(CFStringRef bundleID, CFStringRef test) {
115    return CFStringCompare(bundleID, test, kCFCompareCaseInsensitive) == kCFCompareEqualTo;
116}
117
118void APEBundleMainLateLoad(CFBundleRef inBundle, CFStringRef inAPEToolsApplicationID)
119{
120    if (!ICCF_shouldLoad) return;
121
122    ICCF_bundle = CFBundleGetBundleWithIdentifier(kICBundleIdentifier);
123    CFStringRef bundleID = CFBundleGetIdentifier(CFBundleGetMainBundle());
124    BOOL shouldLoadInNSTextView = YES;
125
126    // XXX handle patching error return from ICCF_PatchMethod
127    if (bundleID != NULL) {
128        if (ICCF_CFBundleIDMatches(bundleID, CFSTR("com.apple.projectbuilder")) ||
129            ICCF_CFBundleIDMatches(bundleID, CFSTR("com.apple.xcode"))) {
130            ICCF_PatchMethod("PBXTextView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
131            ICapeprintf("ICeCoffEE APE: loaded in PBXTextView for PB!\n");
132            shouldLoadInNSTextView = NO;
133        } else if (ICCF_CFBundleIDMatches(bundleID, CFSTR("com.apple.terminal"))) {
134            ICCF_PatchMethod("TermSubview", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:") &&
135            ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "selectedRange") &&
136            ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "attributedSubstringFromRange:") &&
137            ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "mouseDown:") &&
138            ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "mouseUp:") &&
139            ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "draggingEntered:") &&
140            ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "_optionClickEvent:::") ,
141            ICapeprintf("ICeCoffEE APE: loaded in TermSubview for Terminal!\n");
142        } else if (ICCF_CFBundleIDMatches(bundleID, CFSTR("org.mozilla.navigator"))) {
143            ICCF_PatchMethod("ChildView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
144            ICapeprintf("ICeCoffEE APE: loaded in ChildView for Camino\n");
145        }
146    }
147
148    ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "mouseUp:") &&
149        ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "mouseDown:") &&
150        ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "menuForEvent:"),
151        ICapeprintf("ICeCoffEE APE: loaded in WebHTMLView for WebKit/Safari\n");
152   
153    if (shouldLoadInNSTextView) {
154        ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:") &&
155        ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "menuForEvent:") ,
156        ICapeprintf("ICeCoffEE APE: loaded generic NSTextView support\n");
157    }
158
159    gTEClick = APEPatchCreate(&TEClick, &ICCF_TEClick);
160    if (gTEClick != NULL) {
161        ICapeprintf("ICeCoffEE APE: patched TEClick\n");
162    }
163
164    ICCF_ReloadPrefs();
165       
166    return;
167}
168
169// 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.
170OSStatus APEBundleMessage(CFStringRef message,CFDataRef inData,CFDataRef *outData)
171{
172    ICapeprintf("ICeCoffEE APE: message '%@' (inData = %@)\n", message, inData);
173   
174    if (CFStringCompare(message, kICPreferencesChanged, NULL) == kCFCompareEqualTo)             
175    {   // request to reload prefs from our preference pane
176        ICCF_ReloadPrefs();
177    }
178   
179    return noErr;
180}
181
182Boolean ICCF_GetBooleanPref(CFStringRef prefKey, Boolean defaultValue) {
183    Boolean keyExists;
184    Boolean value = CFPreferencesGetAppBooleanValue(prefKey, kICBundleIdentifier, &keyExists);
185    if (keyExists) return value;
186    CFPreferencesSetAppValue(prefKey, defaultValue ? kCFBooleanTrue : kCFBooleanFalse, kICBundleIdentifier);
187    return defaultValue;
188}
189
190CFIndex ICCF_GetCFIndexPref(CFStringRef prefKey, CFIndex defaultValue) {
191    Boolean keyExists;
192    CFIndex value = CFPreferencesGetAppIntegerValue(prefKey, kICBundleIdentifier, &keyExists);
193    if (keyExists) return value;
194    CFNumberRef defaultValueNumber = CFNumberCreate(NULL, kCFNumberCFIndexType, &defaultValue);
195    CFPreferencesSetAppValue(prefKey, defaultValueNumber, kICBundleIdentifier);
196    CFRelease(defaultValueNumber);
197    return defaultValue;
198}
199
200void ICCF_GetCFTypePref(CFStringRef prefKey, CFTypeRef *val, CFTypeID type) {
201    if (*val != NULL) {
202        CFRelease(*val);
203        *val = NULL;
204    }
205   
206    *val = CFPreferencesCopyAppValue(prefKey, kICBundleIdentifier);
207
208    if (*val == NULL) return;
209
210    if (CFGetTypeID(*val) != type) {
211        CFRelease(*val);
212        *val = NULL;
213        return;
214    }
215}
216
217// to test: defaults write net.sabi.ICeCoffEE "Excluded Applications" -array '{CFBundleID = "com.apple.projectbuilder"; }' '{CFBundleID = "net.sabi.Pester"; }' '{CFBundleID = "com.apple.foobar"; }'
218static void ICCF_MigratePrefs() {
219   
220    CFArrayRef prefExcludedApps = CFPreferencesCopyAppValue(kIC12PrefExcluded, kICBundleIdentifier);
221
222    if (prefExcludedApps == NULL) return;
223
224    if (CFGetTypeID(prefExcludedApps) != CFArrayGetTypeID()) {
225        CFRelease(prefExcludedApps);
226        return;
227    }
228   
229    CFMutableArrayRef excludedApps = CFArrayCreateMutableCopy(NULL, 0, prefExcludedApps);
230    CFRelease(prefExcludedApps); prefExcludedApps = NULL;
231
232    ICapeprintf("Excluded apps: %@\n", excludedApps);
233
234    CFIndex excludedAppCount = CFArrayGetCount(excludedApps);
235    CFIndex excludedAppIndex;
236
237    CFDictionaryRef excludedAppSpecifiers = NULL;
238    CFStringRef excludedAppBundleID = NULL;
239    CFURLRef excludedAppURL = NULL;
240    CFBundleRef excludedAppBundle = NULL;
241
242    BOOL postponeMigrationForApp;
243
244    for (excludedAppIndex = excludedAppCount - 1 ; excludedAppIndex >= 0 ; excludedAppIndex--) {
245        if ( (excludedAppSpecifiers = CFArrayGetValueAtIndex(excludedApps, excludedAppIndex)) == NULL || CFGetTypeID(excludedAppSpecifiers) != CFDictionaryGetTypeID()) {
246            CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
247            continue;
248        }
249
250        postponeMigrationForApp = NO;
251        if ( (excludedAppBundleID = CFDictionaryGetValue(excludedAppSpecifiers, kIC12PrefExcludedAppSpecifierBundleID)) != NULL
252             && CFGetTypeID(excludedAppBundleID) == CFStringGetTypeID()
253             && CFStringCompare(excludedAppBundleID, CFSTR("com.apple.projectbuilder"), kCFCompareCaseInsensitive) != kCFCompareEqualTo) {
254            if (LSFindApplicationForInfo(kLSUnknownCreator, excludedAppBundleID, NULL, NULL, &excludedAppURL) == noErr) {
255                excludedAppBundle = CFBundleCreate(NULL, excludedAppURL);
256                if (excludedAppBundle != NULL) {
257                    APEToolsAddToExcludeList(kICBundleIdentifier, excludedAppBundle, CFSTR("Migrated from ICeCoffEE 1.0-1.2"), NO);
258                } else postponeMigrationForApp = YES; // can't create bundle
259            } else postponeMigrationForApp = YES; // can't find app
260        }
261        // don't release excludedAppSpecifiers, used Get
262        // don't release excludedAppBundleID, used Get
263        if (excludedAppURL != NULL) { CFRelease(excludedAppURL); excludedAppURL = NULL; }
264        if (excludedAppBundle != NULL) { CFRelease(excludedAppBundle); excludedAppBundle = NULL; }
265        if (!postponeMigrationForApp) CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
266    }
267
268    ICapeprintf("Excluded apps remaining: %@\n", excludedApps);
269    CFPreferencesSetAppValue(kIC12PrefExcluded, (CFArrayGetCount(excludedApps) == 0 ? NULL : excludedApps), kICBundleIdentifier);
270    CFRelease(excludedApps);
271
272    CFPreferencesAppSynchronize(kICBundleIdentifier);
273}
274
275static void ICCF_ReloadPrefs() {
276    CFPreferencesAppSynchronize(kICBundleIdentifier);
277
278    ICCF_prefs.commandClickEnabled = ICCF_GetBooleanPref(kICCommandClickEnabled, YES);
279    ICCF_prefs.textBlinkEnabled = ICCF_GetBooleanPref(kICTextBlinkEnabled, YES);
280    ICCF_prefs.textBlinkCount = ICCF_GetCFIndexPref(kICTextBlinkCount, 3);
281    ICCF_prefs.servicesInContextualMenu = ICCF_GetBooleanPref(kICServicesInContextualMenu, YES);
282    ICCF_prefs.servicesInMenuBar = ICCF_GetBooleanPref(kICServicesInMenuBar, NO);
283    ICCF_GetCFTypePref(kICServiceOptions, (CFTypeRef *)&ICCF_prefs.serviceOptions, CFDictionaryGetTypeID());
284    ICCF_prefs.terminalRequireOptionForSelfDrag = ICCF_GetBooleanPref(kICTerminalRequireOptionForSelfDrag, NO);
285    ICCF_prefs.errorSoundEnabled = ICCF_GetBooleanPref(kICErrorSoundEnabled, NO);
286    ICCF_prefs.errorDialogEnabled = ICCF_GetBooleanPref(kICErrorDialogEnabled, YES);
287
288    CFPreferencesAppSynchronize(kICBundleIdentifier);
289
290    ICCF_AddRemoveServicesMenu();
291}
Note: See TracBrowser for help on using the repository browser.