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

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

APEMain.m: Fix selection failure with Xcode 3.1.

File size: 14.4 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("XCSourceCodeTextView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:")) {
154 // Xcode 3.x: XCSourceCodeTextView < XCTextView < NSTextView for source
155 // XCDiffTextView < PBXTextView < NSTextView for rich text
156 ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:");
157 ICCF_PatchMethod("XCDiffTextView", "ICeCoffEE", "ICeCoffEESuper", "menuForEvent:");
158 // subclass of PBXTextView; patching both is bad
159 ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "clickedOnLink:atIndex:");
160 } else {
161 // Xcode 2.x: both source and rich text views are PBXTextView
162 ICCF_PatchMethod("PBXTextView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
163 }
164 ICapeprintf("loaded in PBXTextView / XCTextView for Xcode\n");
165 shouldLoadInNSTextView = NO;
166 } else if (ICCF_CFBundleIDMatches(bundleID, CFSTR("com.apple.terminal"))) {
167 if (ICCF_PatchMethod("TTView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:")) {
168 ICCF_PatchMethod("TTView", "ICeCoffEETTView", "ICeCoffEETTViewSuper", "mouseDown:") &&
169 ICCF_PatchMethod("TTView", "ICeCoffEETTView", "ICeCoffEETTViewSuper", "mouseUp:") &&
170 ICCF_PatchMethod("TTView", "ICeCoffEETTView", "ICeCoffEETTViewSuper", "draggingEntered:");
171 ICapeprintf("loaded in TTView for Terminal\n");
172 } else {
173 ICCF_PatchMethod("TermSubview", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:") &&
174 ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "selectedRange") &&
175 ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "attributedSubstringFromRange:") &&
176 ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "mouseDown:") &&
177 ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "mouseUp:") &&
178 ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "draggingEntered:") &&
179 ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICeCoffEETermSubviewSuper", "_optionClickEvent:::");
180 ICapeprintf("loaded in TermSubview for Terminal\n");
181 }
182 } else if (ICCF_CFBundleIDMatches(bundleID, CFSTR("org.mozilla.camino"))) {
183 ICCF_PatchMethod("ChildView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
184 ICapeprintf("loaded in ChildView for Camino\n");
185 }
186 }
187
188 if (shouldLoadInNSTextView) {
189 ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:") &&
190 ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "clickedOnLink:atIndex:") &&
191 ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "menuForEvent:");
192 ICapeprintf("loaded generic NSTextView support\n");
193 }
194
195 _dyld_register_func_for_add_image(ICCF_OnAddImage);
196 ICCF_tryLoading = true;
197 ICCF_OnAddImage(NULL, 0L);
198
199 ICCF_ReloadPrefs();
200
201 return;
202}
203
204// 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.
205OSStatus APEBundleMessage(CFStringRef message,CFDataRef inData,CFDataRef *outData)
206{
207 ICapeprintf("message '%@' (inData = %@)\n", message, inData);
208
209 if (CFStringCompare(message, kICPreferencesChanged, 0) == kCFCompareEqualTo)
210 { // request to reload prefs from our preference pane
211 ICCF_ReloadPrefs();
212 }
213
214 return noErr;
215}
216
217Boolean ICCF_GetBooleanPref(CFStringRef prefKey, Boolean defaultValue) {
218 Boolean keyExists;
219 Boolean value = CFPreferencesGetAppBooleanValue(prefKey, kICBundleIdentifier, &keyExists);
220 if (keyExists) return value;
221 CFPreferencesSetAppValue(prefKey, defaultValue ? kCFBooleanTrue : kCFBooleanFalse, kICBundleIdentifier);
222 return defaultValue;
223}
224
225CFIndex ICCF_GetCFIndexPref(CFStringRef prefKey, CFIndex defaultValue) {
226 Boolean keyExists;
227 CFIndex value = CFPreferencesGetAppIntegerValue(prefKey, kICBundleIdentifier, &keyExists);
228 if (keyExists) return value;
229 CFNumberRef defaultValueNumber = CFNumberCreate(NULL, kCFNumberCFIndexType, &defaultValue);
230 CFPreferencesSetAppValue(prefKey, defaultValueNumber, kICBundleIdentifier);
231 CFRelease(defaultValueNumber);
232 return defaultValue;
233}
234
235void ICCF_GetCFTypePref(CFStringRef prefKey, CFTypeRef *val, CFTypeID type) {
236 if (*val != NULL) {
237 CFRelease(*val);
238 *val = NULL;
239 }
240
241 *val = CFPreferencesCopyAppValue(prefKey, kICBundleIdentifier);
242
243 if (*val == NULL) return;
244
245 if (CFGetTypeID(*val) != type) {
246 CFRelease(*val);
247 *val = NULL;
248 return;
249 }
250}
251
252// to test: defaults write net.sabi.ICeCoffEE "Excluded Applications" -array '{CFBundleID = "com.apple.projectbuilder"; }' '{CFBundleID = "net.sabi.Pester"; }' '{CFBundleID = "com.apple.foobar"; }'
253static void ICCF_MigratePrefs() {
254
255 CFArrayRef prefExcludedApps = CFPreferencesCopyAppValue(kIC12PrefExcluded, kICBundleIdentifier);
256
257 if (prefExcludedApps == NULL) return;
258
259 if (CFGetTypeID(prefExcludedApps) != CFArrayGetTypeID()) {
260 CFRelease(prefExcludedApps);
261 return;
262 }
263
264 CFMutableArrayRef excludedApps = CFArrayCreateMutableCopy(NULL, 0, prefExcludedApps);
265 CFRelease(prefExcludedApps); prefExcludedApps = NULL;
266
267 ICapeprintf("Excluded apps: %@\n", excludedApps);
268
269 CFIndex excludedAppCount = CFArrayGetCount(excludedApps);
270 CFIndex excludedAppIndex;
271
272 CFDictionaryRef excludedAppSpecifiers = NULL;
273 CFStringRef excludedAppBundleID = NULL;
274 CFURLRef excludedAppURL = NULL;
275 CFBundleRef excludedAppBundle = NULL;
276
277 BOOL postponeMigrationForApp;
278
279 for (excludedAppIndex = excludedAppCount - 1 ; excludedAppIndex >= 0 ; excludedAppIndex--) {
280 if ( (excludedAppSpecifiers = CFArrayGetValueAtIndex(excludedApps, excludedAppIndex)) == NULL || CFGetTypeID(excludedAppSpecifiers) != CFDictionaryGetTypeID()) {
281 CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
282 continue;
283 }
284
285 postponeMigrationForApp = NO;
286 if ( (excludedAppBundleID = CFDictionaryGetValue(excludedAppSpecifiers, kIC12PrefExcludedAppSpecifierBundleID)) != NULL
287 && CFGetTypeID(excludedAppBundleID) == CFStringGetTypeID()
288 && CFStringCompare(excludedAppBundleID, CFSTR("com.apple.projectbuilder"), kCFCompareCaseInsensitive) != kCFCompareEqualTo) {
289 if (LSFindApplicationForInfo(kLSUnknownCreator, excludedAppBundleID, NULL, NULL, &excludedAppURL) == noErr) {
290 excludedAppBundle = CFBundleCreate(NULL, excludedAppURL);
291 if (excludedAppBundle != NULL) {
292 APEToolsAddToExcludeList(kICBundleIdentifier, excludedAppBundle, CFSTR("Migrated from ICeCoffEE 1.0-1.2"), NO);
293 } else postponeMigrationForApp = YES; // can't create bundle
294 } else postponeMigrationForApp = YES; // can't find app
295 }
296 // don't release excludedAppSpecifiers, used Get
297 // don't release excludedAppBundleID, used Get
298 if (excludedAppURL != NULL) { CFRelease(excludedAppURL); excludedAppURL = NULL; }
299 if (excludedAppBundle != NULL) { CFRelease(excludedAppBundle); excludedAppBundle = NULL; }
300 if (!postponeMigrationForApp) CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
301 }
302
303 ICapeprintf("Excluded apps remaining: %@\n", excludedApps);
304 CFPreferencesSetAppValue(kIC12PrefExcluded, (CFArrayGetCount(excludedApps) == 0 ? NULL : excludedApps), kICBundleIdentifier);
305 CFRelease(excludedApps);
306
307 CFPreferencesAppSynchronize(kICBundleIdentifier);
308}
309
310static void ICCF_ReloadPrefs() {
311 CFPreferencesAppSynchronize(kICBundleIdentifier);
312
313 ICCF_prefs.commandClickEnabled = ICCF_GetBooleanPref(kICCommandClickEnabled, YES);
314 ICCF_prefs.textBlinkEnabled = ICCF_GetBooleanPref(kICTextBlinkEnabled, YES);
315 ICCF_prefs.textBlinkCount = ICCF_GetCFIndexPref(kICTextBlinkCount, 3);
316 ICCF_prefs.servicesInContextualMenu = ICCF_GetBooleanPref(kICServicesInContextualMenu, YES);
317 ICCF_prefs.servicesInMenuBar = ICCF_GetBooleanPref(kICServicesInMenuBar, NO);
318 ICCF_GetCFTypePref(kICServiceOptions, (CFTypeRef *)&ICCF_prefs.serviceOptions, CFDictionaryGetTypeID());
319 ICCF_prefs.terminalRequireOptionForSelfDrag = ICCF_GetBooleanPref(kICTerminalRequireOptionForSelfDrag, NO);
320 ICCF_prefs.errorSoundEnabled = ICCF_GetBooleanPref(kICErrorSoundEnabled, NO);
321 ICCF_prefs.errorDialogEnabled = ICCF_GetBooleanPref(kICErrorDialogEnabled, YES);
322
323 CFPreferencesAppSynchronize(kICBundleIdentifier);
324
325 ICCF_AddRemoveServicesMenu();
326}
Note: See TracBrowser for help on using the repository browser.