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

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

Update for SCR 1.5, no longer registering explicitly.

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