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

Last change on this file since 460 was 456, checked in by Nicholas Riley, 16 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.