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

Last change on this file since 473 was 469, checked in by Nicholas Riley, 16 years ago

Services in PDFView/WebPDFView; activate on dynamic WebKit/PDFKit loading.

File size: 14.2 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 <mach-o/dyld.h>
18#import <objc/objc-runtime.h>
19#import "ICeCoffEE.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 background-only applications.
69static Boolean ICCF_shouldLoad;
70
71static Boolean ICCF_IsOne(CFTypeRef value) {
72 if (value == NULL)
73 return false;
74 CFTypeID typeID = CFGetTypeID(value);
75 if (typeID == CFBooleanGetTypeID())
76 return CFBooleanGetValue((CFBooleanRef)value);
77 else if (typeID == CFNumberGetTypeID()) {
78 static const int one = 1;
79 static CFNumberRef oneRef = NULL;
80 if (oneRef == NULL) oneRef = CFNumberCreate(NULL, kCFNumberIntType, &one);
81 return CFNumberCompare((CFNumberRef)value, oneRef, NULL) == kCFCompareEqualTo;
82 } else if (typeID == CFStringGetTypeID()) {
83 return CFStringCompare((CFStringRef)value, CFSTR("1"), 0) == kCFCompareEqualTo;
84 }
85 return false;
86}
87
88void APEBundleMainEarlyLoad(CFBundleRef inBundle, CFStringRef inAPEToolsApplicationID)
89{
90 ICCF_MigratePrefs();
91
92 UInt32 icVersion = CFBundleGetVersionNumber(inBundle);
93 ICapeprintf("bundle version is %ld (0x%x)\n", icVersion, icVersion);
94 CFNumberRef icVersionRef = CFNumberCreate(NULL, kCFNumberLongType, &icVersion);
95 CFPreferencesSetAppValue(kICLastLoadedVersion, icVersionRef, kICBundleIdentifier);
96 SAFE_RELEASE(icVersionRef);
97
98 ICCF_shouldLoad = false;
99
100 CFBundleRef appBundle = CFBundleGetMainBundle();
101 if (appBundle == NULL) {
102 apeprintf("can't get CFBundle for current process; not loading\n");
103 return;
104 }
105
106 if (ICCF_IsOne(CFBundleGetValueForInfoDictionaryKey(appBundle, CFSTR("LSUIElement"))) ||
107 ICCF_IsOne(CFBundleGetValueForInfoDictionaryKey(appBundle, CFSTR("NSUIElement"))) ||
108 ICCF_IsOne(CFBundleGetValueForInfoDictionaryKey(appBundle, CFSTR("LSBackgroundOnly"))) ||
109 ICCF_IsOne(CFBundleGetValueForInfoDictionaryKey(appBundle, CFSTR("NSBackgroundOnly")))) {
110 ICapeprintf("not loading as this application is background-only\n");
111 return;
112 }
113 ICCF_shouldLoad = true;
114}
115
116static Boolean ICCF_CFBundleIDMatches(CFStringRef bundleID, CFStringRef test) {
117 return CFStringCompare(bundleID, test, kCFCompareCaseInsensitive) == kCFCompareEqualTo;
118}
119
120static Boolean ICCF_tryLoading = false;
121static Boolean ICCF_loadedInWebKit = false;
122static Boolean ICCF_loadedInPDFKit = false;
123
124static void ICCF_OnAddImage(const struct mach_header *mh, intptr_t vmaddr_slide) {
125 if (!ICCF_tryLoading) // when initially registered, called a lot
126 return;
127 if (!ICCF_loadedInWebKit &&
128 ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "mouseUp:") &&
129 ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "mouseDown:") &&
130 ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "menuForEvent:") &&
131 ICCF_PatchMethod("WebPDFView", "ICeCoffEEWebPDFView", "ICeCoffEEWebPDFViewSuper", "menuForEvent:")) {
132 ICapeprintf("loaded in WebHTMLView/WebPDFView for WebKit/Safari 3\n");
133 ICCF_loadedInWebKit = true;
134 }
135 if (!ICCF_loadedInPDFKit &&
136 ICCF_PatchMethod("PDFView", "ICeCoffEEPDFView", "ICeCoffEEPDFViewSuper", "menuForEvent:")) {
137 ICapeprintf("loaded in PDFView for PDFKit\n");
138 ICCF_loadedInPDFKit = true;
139 }
140}
141
142void APEBundleMainLateLoad(CFBundleRef inBundle, CFStringRef inAPEToolsApplicationID)
143{
144 if (!ICCF_shouldLoad) return;
145
146 ICCF_bundle = inBundle;
147 CFStringRef bundleID = CFBundleGetIdentifier(CFBundleGetMainBundle());
148 BOOL shouldLoadInNSTextView = YES;
149
150 // XXX handle patching error return from ICCF_PatchMethod
151 if (bundleID != NULL) {
152 if (ICCF_CFBundleIDMatches(bundleID, CFSTR("com.apple.xcode"))) {
153 if (ICCF_PatchMethod("XCTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:")) {
154 ICCF_PatchMethod("XCSourceCodeTextView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
155 ICCF_PatchMethod("XCDiffTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:");
156 ICCF_PatchMethod("XCDiffTextView", "ICeCoffEE", "ICeCoffEESuper", "menuForEvent:"); // subclass of PBXTextView; patching both is bad
157 ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "clickedOnLink:atIndex:");
158 } else {
159 ICCF_PatchMethod("PBXTextView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
160 }
161 ICapeprintf("loaded in PBXTextView / XCTextView for Xcode\n");
162 shouldLoadInNSTextView = NO;
163 } else if (ICCF_CFBundleIDMatches(bundleID, CFSTR("com.apple.terminal"))) {
164 if (ICCF_PatchMethod("TTView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:")) {
165 ICCF_PatchMethod("TTView", "ICeCoffEETTView", "ICeCoffEETTViewSuper", "mouseDown:") &&
166 ICCF_PatchMethod("TTView", "ICeCoffEETTView", "ICeCoffEETTViewSuper", "mouseUp:") &&
167 ICCF_PatchMethod("TTView", "ICeCoffEETTView", "ICeCoffEETTViewSuper", "draggingEntered:");
168 ICapeprintf("loaded in TTView for Terminal\n");
169 } else {
170 ICCF_PatchMethod("TermSubview", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:") &&
171 ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "selectedRange") &&
172 ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "attributedSubstringFromRange:") &&
173 ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "mouseDown:") &&
174 ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "mouseUp:") &&
175 ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "draggingEntered:") &&
176 ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "_optionClickEvent:::");
177 ICapeprintf("loaded in TermSubview for Terminal\n");
178 }
179 } else if (ICCF_CFBundleIDMatches(bundleID, CFSTR("org.mozilla.camino"))) {
180 ICCF_PatchMethod("ChildView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
181 ICapeprintf("loaded in ChildView for Camino\n");
182 }
183 }
184
185 if (shouldLoadInNSTextView) {
186 ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:") &&
187 ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "clickedOnLink:atIndex:") &&
188 ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "menuForEvent:");
189 ICapeprintf("loaded generic NSTextView support\n");
190 }
191
192 _dyld_register_func_for_add_image(ICCF_OnAddImage);
193 ICCF_tryLoading = true;
194 ICCF_OnAddImage(NULL, 0L);
195
196 ICCF_ReloadPrefs();
197
198 return;
199}
200
201// 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.
202OSStatus APEBundleMessage(CFStringRef message,CFDataRef inData,CFDataRef *outData)
203{
204 ICapeprintf("message '%@' (inData = %@)\n", message, inData);
205
206 if (CFStringCompare(message, kICPreferencesChanged, 0) == kCFCompareEqualTo)
207 { // request to reload prefs from our preference pane
208 ICCF_ReloadPrefs();
209 }
210
211 return noErr;
212}
213
214Boolean ICCF_GetBooleanPref(CFStringRef prefKey, Boolean defaultValue) {
215 Boolean keyExists;
216 Boolean value = CFPreferencesGetAppBooleanValue(prefKey, kICBundleIdentifier, &keyExists);
217 if (keyExists) return value;
218 CFPreferencesSetAppValue(prefKey, defaultValue ? kCFBooleanTrue : kCFBooleanFalse, kICBundleIdentifier);
219 return defaultValue;
220}
221
222CFIndex ICCF_GetCFIndexPref(CFStringRef prefKey, CFIndex defaultValue) {
223 Boolean keyExists;
224 CFIndex value = CFPreferencesGetAppIntegerValue(prefKey, kICBundleIdentifier, &keyExists);
225 if (keyExists) return value;
226 CFNumberRef defaultValueNumber = CFNumberCreate(NULL, kCFNumberCFIndexType, &defaultValue);
227 CFPreferencesSetAppValue(prefKey, defaultValueNumber, kICBundleIdentifier);
228 CFRelease(defaultValueNumber);
229 return defaultValue;
230}
231
232void ICCF_GetCFTypePref(CFStringRef prefKey, CFTypeRef *val, CFTypeID type) {
233 if (*val != NULL) {
234 CFRelease(*val);
235 *val = NULL;
236 }
237
238 *val = CFPreferencesCopyAppValue(prefKey, kICBundleIdentifier);
239
240 if (*val == NULL) return;
241
242 if (CFGetTypeID(*val) != type) {
243 CFRelease(*val);
244 *val = NULL;
245 return;
246 }
247}
248
249// to test: defaults write net.sabi.ICeCoffEE "Excluded Applications" -array '{CFBundleID = "com.apple.projectbuilder"; }' '{CFBundleID = "net.sabi.Pester"; }' '{CFBundleID = "com.apple.foobar"; }'
250static void ICCF_MigratePrefs() {
251
252 CFArrayRef prefExcludedApps = CFPreferencesCopyAppValue(kIC12PrefExcluded, kICBundleIdentifier);
253
254 if (prefExcludedApps == NULL) return;
255
256 if (CFGetTypeID(prefExcludedApps) != CFArrayGetTypeID()) {
257 CFRelease(prefExcludedApps);
258 return;
259 }
260
261 CFMutableArrayRef excludedApps = CFArrayCreateMutableCopy(NULL, 0, prefExcludedApps);
262 CFRelease(prefExcludedApps); prefExcludedApps = NULL;
263
264 ICapeprintf("Excluded apps: %@\n", excludedApps);
265
266 CFIndex excludedAppCount = CFArrayGetCount(excludedApps);
267 CFIndex excludedAppIndex;
268
269 CFDictionaryRef excludedAppSpecifiers = NULL;
270 CFStringRef excludedAppBundleID = NULL;
271 CFURLRef excludedAppURL = NULL;
272 CFBundleRef excludedAppBundle = NULL;
273
274 BOOL postponeMigrationForApp;
275
276 for (excludedAppIndex = excludedAppCount - 1 ; excludedAppIndex >= 0 ; excludedAppIndex--) {
277 if ( (excludedAppSpecifiers = CFArrayGetValueAtIndex(excludedApps, excludedAppIndex)) == NULL || CFGetTypeID(excludedAppSpecifiers) != CFDictionaryGetTypeID()) {
278 CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
279 continue;
280 }
281
282 postponeMigrationForApp = NO;
283 if ( (excludedAppBundleID = CFDictionaryGetValue(excludedAppSpecifiers, kIC12PrefExcludedAppSpecifierBundleID)) != NULL
284 && CFGetTypeID(excludedAppBundleID) == CFStringGetTypeID()
285 && CFStringCompare(excludedAppBundleID, CFSTR("com.apple.projectbuilder"), kCFCompareCaseInsensitive) != kCFCompareEqualTo) {
286 if (LSFindApplicationForInfo(kLSUnknownCreator, excludedAppBundleID, NULL, NULL, &excludedAppURL) == noErr) {
287 excludedAppBundle = CFBundleCreate(NULL, excludedAppURL);
288 if (excludedAppBundle != NULL) {
289 APEToolsAddToExcludeList(kICBundleIdentifier, excludedAppBundle, CFSTR("Migrated from ICeCoffEE 1.0-1.2"), NO);
290 } else postponeMigrationForApp = YES; // can't create bundle
291 } else postponeMigrationForApp = YES; // can't find app
292 }
293 // don't release excludedAppSpecifiers, used Get
294 // don't release excludedAppBundleID, used Get
295 if (excludedAppURL != NULL) { CFRelease(excludedAppURL); excludedAppURL = NULL; }
296 if (excludedAppBundle != NULL) { CFRelease(excludedAppBundle); excludedAppBundle = NULL; }
297 if (!postponeMigrationForApp) CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
298 }
299
300 ICapeprintf("Excluded apps remaining: %@\n", excludedApps);
301 CFPreferencesSetAppValue(kIC12PrefExcluded, (CFArrayGetCount(excludedApps) == 0 ? NULL : excludedApps), kICBundleIdentifier);
302 CFRelease(excludedApps);
303
304 CFPreferencesAppSynchronize(kICBundleIdentifier);
305}
306
307static void ICCF_ReloadPrefs() {
308 CFPreferencesAppSynchronize(kICBundleIdentifier);
309
310 ICCF_prefs.commandClickEnabled = ICCF_GetBooleanPref(kICCommandClickEnabled, YES);
311 ICCF_prefs.textBlinkEnabled = ICCF_GetBooleanPref(kICTextBlinkEnabled, YES);
312 ICCF_prefs.textBlinkCount = ICCF_GetCFIndexPref(kICTextBlinkCount, 3);
313 ICCF_prefs.servicesInContextualMenu = ICCF_GetBooleanPref(kICServicesInContextualMenu, YES);
314 ICCF_prefs.servicesInMenuBar = ICCF_GetBooleanPref(kICServicesInMenuBar, NO);
315 ICCF_GetCFTypePref(kICServiceOptions, (CFTypeRef *)&ICCF_prefs.serviceOptions, CFDictionaryGetTypeID());
316 ICCF_prefs.terminalRequireOptionForSelfDrag = ICCF_GetBooleanPref(kICTerminalRequireOptionForSelfDrag, NO);
317 ICCF_prefs.errorSoundEnabled = ICCF_GetBooleanPref(kICErrorSoundEnabled, NO);
318 ICCF_prefs.errorDialogEnabled = ICCF_GetBooleanPref(kICErrorDialogEnabled, YES);
319
320 CFPreferencesAppSynchronize(kICBundleIdentifier);
321
322 ICCF_AddRemoveServicesMenu();
323}
Note: See TracBrowser for help on using the repository browser.