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

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

Operate on the link target, rather than the text, of NSTextView hyperlinks

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