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

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

APEMain.m: Xcode 3 URL launching support

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