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 | #import "ICeCoffEETextEdit.h"
|
---|
20 | #import "CPS.h"
|
---|
21 |
|
---|
22 | //¥¥¥ Our settings
|
---|
23 |
|
---|
24 | //¥¥¥ Function prototypes
|
---|
25 | static void ICCF_ReloadPrefs(); // reloads our preferences
|
---|
26 | static void ICCF_MigratePrefs(); // migrates prefs from 1.0Ð1.2
|
---|
27 |
|
---|
28 | //¥¥¥ Enter sandman, the code begins --
|
---|
29 |
|
---|
30 | #define ICCF_GET_PATCHCLASS(patchclass) \
|
---|
31 | struct objc_class *patchclass = objc_getClass(patchclass ## Name); \
|
---|
32 | if (patchclass == NULL) { \
|
---|
33 | ICapeprintf("can't get %s\n", patchclass ## Name); \
|
---|
34 | return NO; \
|
---|
35 | }
|
---|
36 |
|
---|
37 | #define ICCF_GET_METHOD(name, patchclass, sel) \
|
---|
38 | Method name = class_getInstanceMethod(patchclass, sel); \
|
---|
39 | if (name == NULL) { \
|
---|
40 | ICapeprintf("can't get %s\n", patchclass ## Name); \
|
---|
41 | return NO; \
|
---|
42 | }
|
---|
43 |
|
---|
44 | BOOL ICCF_PatchMethod(char *patcheeClassName,
|
---|
45 | char *patchClassName,
|
---|
46 | char *patchSuperclassName,
|
---|
47 | char *selectorString) {
|
---|
48 |
|
---|
49 | ICCF_GET_PATCHCLASS(patcheeClass);
|
---|
50 | ICCF_GET_PATCHCLASS(patchClass);
|
---|
51 | ICCF_GET_PATCHCLASS(patchSuperclass);
|
---|
52 |
|
---|
53 | SEL selector = sel_getUid(selectorString);
|
---|
54 |
|
---|
55 | ICCF_GET_METHOD(patcheeMethod, patcheeClass, selector);
|
---|
56 | ICCF_GET_METHOD(patchMethod, patchClass, selector);
|
---|
57 | ICCF_GET_METHOD(patchSuperMethod, patchSuperclass, selector);
|
---|
58 |
|
---|
59 | if (APEPatchCreate(patchSuperMethod->method_imp,
|
---|
60 | APEPatchCreate(patcheeMethod->method_imp, patchMethod->method_imp)) == NULL) {
|
---|
61 | ICapeprintf("can't patch class %s with [%s %s] super %s", patcheeClassName, patchClassName, selectorString, patchSuperclassName);
|
---|
62 | return NO;
|
---|
63 | }
|
---|
64 | return YES;
|
---|
65 | }
|
---|
66 |
|
---|
67 | CFBundleRef ICCF_bundle;
|
---|
68 |
|
---|
69 | // 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 non-GUI appllications.
|
---|
70 | Boolean ICCF_shouldLoad;
|
---|
71 |
|
---|
72 | void APEBundleMainEarlyLoad(CFBundleRef inBundle, CFStringRef inAPEToolsApplicationID)
|
---|
73 | {
|
---|
74 | ICCF_MigratePrefs();
|
---|
75 |
|
---|
76 | UInt32 icVersion = CFBundleGetVersionNumber(inBundle);
|
---|
77 | ICapeprintf("ICeCoffEE APE: bundle version is %ld (0x%x)\n", icVersion, icVersion);
|
---|
78 | CFNumberRef icVersionRef = CFNumberCreate(NULL, kCFNumberLongType, &icVersion);
|
---|
79 | CFPreferencesSetAppValue(kICLastLoadedVersion, icVersionRef, kICBundleIdentifier);
|
---|
80 | SAFE_RELEASE(icVersionRef);
|
---|
81 |
|
---|
82 | ICCF_shouldLoad = false;
|
---|
83 |
|
---|
84 | CPSProcessSerNum psn;
|
---|
85 | OSStatus err = CPSGetCurrentProcess(&psn);
|
---|
86 | if (err != noErr) {
|
---|
87 | #if ICCF_DEBUG
|
---|
88 | apeprintf("ICeCoffEE APE: Can't get process serial number for current process (error %ld); still loading because of ICCF_DEBUG\n", err);
|
---|
89 | #else
|
---|
90 | apeprintf("ICeCoffEE APE: Can't get process serial number for current process (error %ld); not loading in this application\n", err);
|
---|
91 | return;
|
---|
92 | #endif
|
---|
93 | }
|
---|
94 |
|
---|
95 | CPSProcessInfoRec info;
|
---|
96 | err = CPSGetProcessInfo(&psn, &info, NULL, 0, NULL, NULL, 0);
|
---|
97 | if (err != noErr) {
|
---|
98 | #if ICCF_DEBUG
|
---|
99 | apeprintf("ICeCoffEE APE: Can't get process information (error %ld); still loading because of ICCF_DEBUG\n", err);
|
---|
100 | #else
|
---|
101 | apeprintf("ICeCoffEE APE: Can't get process information (error %ld); not loading in this application\n", err);
|
---|
102 | return;
|
---|
103 | #endif
|
---|
104 | } else {
|
---|
105 | ICapeprintf("ICeCoffEE APE: got process attributes = 0x%lx\n", info.Attributes);
|
---|
106 | if (info.Attributes & (kCPSBGOnlyAttr | kCPSUIElementAttr | kCPSFullScreenAttr)) {
|
---|
107 | ICapeprintf("ICeCoffEE APE: not loading as this application is background-only\n");
|
---|
108 | return;
|
---|
109 | }
|
---|
110 | }
|
---|
111 | ICCF_shouldLoad = true;
|
---|
112 | }
|
---|
113 |
|
---|
114 | Boolean ICCF_CFBundleIDMatches(CFStringRef bundleID, CFStringRef test) {
|
---|
115 | return CFStringCompare(bundleID, test, kCFCompareCaseInsensitive) == kCFCompareEqualTo;
|
---|
116 | }
|
---|
117 |
|
---|
118 | void APEBundleMainLateLoad(CFBundleRef inBundle, CFStringRef inAPEToolsApplicationID)
|
---|
119 | {
|
---|
120 | if (!ICCF_shouldLoad) return;
|
---|
121 |
|
---|
122 | ICCF_bundle = CFBundleGetBundleWithIdentifier(kICBundleIdentifier);
|
---|
123 | CFStringRef bundleID = CFBundleGetIdentifier(CFBundleGetMainBundle());
|
---|
124 | BOOL shouldLoadInNSTextView = YES;
|
---|
125 |
|
---|
126 | // XXX handle patching error return from ICCF_PatchMethod
|
---|
127 | if (bundleID != NULL) {
|
---|
128 | if (ICCF_CFBundleIDMatches(bundleID, CFSTR("com.apple.projectbuilder")) ||
|
---|
129 | ICCF_CFBundleIDMatches(bundleID, CFSTR("com.apple.xcode"))) {
|
---|
130 | ICCF_PatchMethod("PBXTextView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
|
---|
131 | ICapeprintf("ICeCoffEE APE: loaded in PBXTextView for PB!\n");
|
---|
132 | shouldLoadInNSTextView = NO;
|
---|
133 | } else if (ICCF_CFBundleIDMatches(bundleID, CFSTR("com.apple.terminal"))) {
|
---|
134 | ICCF_PatchMethod("TermSubview", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:") &&
|
---|
135 | ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "selectedRange") &&
|
---|
136 | ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "attributedSubstringFromRange:") &&
|
---|
137 | ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "mouseDown:") &&
|
---|
138 | ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "mouseUp:") &&
|
---|
139 | ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "draggingEntered:") &&
|
---|
140 | ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "_optionClickEvent:::") ,
|
---|
141 | ICapeprintf("ICeCoffEE APE: loaded in TermSubview for Terminal!\n");
|
---|
142 | } else if (ICCF_CFBundleIDMatches(bundleID, CFSTR("org.mozilla.navigator"))) {
|
---|
143 | ICCF_PatchMethod("ChildView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
|
---|
144 | ICapeprintf("ICeCoffEE APE: loaded in ChildView for Camino\n");
|
---|
145 | }
|
---|
146 | }
|
---|
147 |
|
---|
148 | ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "mouseUp:") &&
|
---|
149 | ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "mouseDown:") &&
|
---|
150 | ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "menuForEvent:"),
|
---|
151 | ICapeprintf("ICeCoffEE APE: loaded in WebHTMLView for WebKit/Safari\n");
|
---|
152 |
|
---|
153 | if (shouldLoadInNSTextView) {
|
---|
154 | ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:") &&
|
---|
155 | ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "menuForEvent:") ,
|
---|
156 | ICapeprintf("ICeCoffEE APE: loaded generic NSTextView support\n");
|
---|
157 | }
|
---|
158 |
|
---|
159 | gTEClick = APEPatchCreate(&TEClick, &ICCF_TEClick);
|
---|
160 | if (gTEClick != NULL) {
|
---|
161 | ICapeprintf("ICeCoffEE APE: patched TEClick\n");
|
---|
162 | }
|
---|
163 |
|
---|
164 | ICCF_ReloadPrefs();
|
---|
165 |
|
---|
166 | return;
|
---|
167 | }
|
---|
168 |
|
---|
169 | // 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.
|
---|
170 | OSStatus APEBundleMessage(CFStringRef message,CFDataRef inData,CFDataRef *outData)
|
---|
171 | {
|
---|
172 | ICapeprintf("ICeCoffEE APE: message '%@' (inData = %@)\n", message, inData);
|
---|
173 |
|
---|
174 | if (CFStringCompare(message, kICPreferencesChanged, 0) == kCFCompareEqualTo)
|
---|
175 | { // request to reload prefs from our preference pane
|
---|
176 | ICCF_ReloadPrefs();
|
---|
177 | }
|
---|
178 |
|
---|
179 | return noErr;
|
---|
180 | }
|
---|
181 |
|
---|
182 | Boolean ICCF_GetBooleanPref(CFStringRef prefKey, Boolean defaultValue) {
|
---|
183 | Boolean keyExists;
|
---|
184 | Boolean value = CFPreferencesGetAppBooleanValue(prefKey, kICBundleIdentifier, &keyExists);
|
---|
185 | if (keyExists) return value;
|
---|
186 | CFPreferencesSetAppValue(prefKey, defaultValue ? kCFBooleanTrue : kCFBooleanFalse, kICBundleIdentifier);
|
---|
187 | return defaultValue;
|
---|
188 | }
|
---|
189 |
|
---|
190 | CFIndex ICCF_GetCFIndexPref(CFStringRef prefKey, CFIndex defaultValue) {
|
---|
191 | Boolean keyExists;
|
---|
192 | CFIndex value = CFPreferencesGetAppIntegerValue(prefKey, kICBundleIdentifier, &keyExists);
|
---|
193 | if (keyExists) return value;
|
---|
194 | CFNumberRef defaultValueNumber = CFNumberCreate(NULL, kCFNumberCFIndexType, &defaultValue);
|
---|
195 | CFPreferencesSetAppValue(prefKey, defaultValueNumber, kICBundleIdentifier);
|
---|
196 | CFRelease(defaultValueNumber);
|
---|
197 | return defaultValue;
|
---|
198 | }
|
---|
199 |
|
---|
200 | void ICCF_GetCFTypePref(CFStringRef prefKey, CFTypeRef *val, CFTypeID type) {
|
---|
201 | if (*val != NULL) {
|
---|
202 | CFRelease(*val);
|
---|
203 | *val = NULL;
|
---|
204 | }
|
---|
205 |
|
---|
206 | *val = CFPreferencesCopyAppValue(prefKey, kICBundleIdentifier);
|
---|
207 |
|
---|
208 | if (*val == NULL) return;
|
---|
209 |
|
---|
210 | if (CFGetTypeID(*val) != type) {
|
---|
211 | CFRelease(*val);
|
---|
212 | *val = NULL;
|
---|
213 | return;
|
---|
214 | }
|
---|
215 | }
|
---|
216 |
|
---|
217 | // to test: defaults write net.sabi.ICeCoffEE "Excluded Applications" -array '{CFBundleID = "com.apple.projectbuilder"; }' '{CFBundleID = "net.sabi.Pester"; }' '{CFBundleID = "com.apple.foobar"; }'
|
---|
218 | static void ICCF_MigratePrefs() {
|
---|
219 |
|
---|
220 | CFArrayRef prefExcludedApps = CFPreferencesCopyAppValue(kIC12PrefExcluded, kICBundleIdentifier);
|
---|
221 |
|
---|
222 | if (prefExcludedApps == NULL) return;
|
---|
223 |
|
---|
224 | if (CFGetTypeID(prefExcludedApps) != CFArrayGetTypeID()) {
|
---|
225 | CFRelease(prefExcludedApps);
|
---|
226 | return;
|
---|
227 | }
|
---|
228 |
|
---|
229 | CFMutableArrayRef excludedApps = CFArrayCreateMutableCopy(NULL, 0, prefExcludedApps);
|
---|
230 | CFRelease(prefExcludedApps); prefExcludedApps = NULL;
|
---|
231 |
|
---|
232 | ICapeprintf("Excluded apps: %@\n", excludedApps);
|
---|
233 |
|
---|
234 | CFIndex excludedAppCount = CFArrayGetCount(excludedApps);
|
---|
235 | CFIndex excludedAppIndex;
|
---|
236 |
|
---|
237 | CFDictionaryRef excludedAppSpecifiers = NULL;
|
---|
238 | CFStringRef excludedAppBundleID = NULL;
|
---|
239 | CFURLRef excludedAppURL = NULL;
|
---|
240 | CFBundleRef excludedAppBundle = NULL;
|
---|
241 |
|
---|
242 | BOOL postponeMigrationForApp;
|
---|
243 |
|
---|
244 | for (excludedAppIndex = excludedAppCount - 1 ; excludedAppIndex >= 0 ; excludedAppIndex--) {
|
---|
245 | if ( (excludedAppSpecifiers = CFArrayGetValueAtIndex(excludedApps, excludedAppIndex)) == NULL || CFGetTypeID(excludedAppSpecifiers) != CFDictionaryGetTypeID()) {
|
---|
246 | CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
|
---|
247 | continue;
|
---|
248 | }
|
---|
249 |
|
---|
250 | postponeMigrationForApp = NO;
|
---|
251 | if ( (excludedAppBundleID = CFDictionaryGetValue(excludedAppSpecifiers, kIC12PrefExcludedAppSpecifierBundleID)) != NULL
|
---|
252 | && CFGetTypeID(excludedAppBundleID) == CFStringGetTypeID()
|
---|
253 | && CFStringCompare(excludedAppBundleID, CFSTR("com.apple.projectbuilder"), kCFCompareCaseInsensitive) != kCFCompareEqualTo) {
|
---|
254 | if (LSFindApplicationForInfo(kLSUnknownCreator, excludedAppBundleID, NULL, NULL, &excludedAppURL) == noErr) {
|
---|
255 | excludedAppBundle = CFBundleCreate(NULL, excludedAppURL);
|
---|
256 | if (excludedAppBundle != NULL) {
|
---|
257 | APEToolsAddToExcludeList(kICBundleIdentifier, excludedAppBundle, CFSTR("Migrated from ICeCoffEE 1.0-1.2"), NO);
|
---|
258 | } else postponeMigrationForApp = YES; // can't create bundle
|
---|
259 | } else postponeMigrationForApp = YES; // can't find app
|
---|
260 | }
|
---|
261 | // don't release excludedAppSpecifiers, used Get
|
---|
262 | // don't release excludedAppBundleID, used Get
|
---|
263 | if (excludedAppURL != NULL) { CFRelease(excludedAppURL); excludedAppURL = NULL; }
|
---|
264 | if (excludedAppBundle != NULL) { CFRelease(excludedAppBundle); excludedAppBundle = NULL; }
|
---|
265 | if (!postponeMigrationForApp) CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
|
---|
266 | }
|
---|
267 |
|
---|
268 | ICapeprintf("Excluded apps remaining: %@\n", excludedApps);
|
---|
269 | CFPreferencesSetAppValue(kIC12PrefExcluded, (CFArrayGetCount(excludedApps) == 0 ? NULL : excludedApps), kICBundleIdentifier);
|
---|
270 | CFRelease(excludedApps);
|
---|
271 |
|
---|
272 | CFPreferencesAppSynchronize(kICBundleIdentifier);
|
---|
273 | }
|
---|
274 |
|
---|
275 | static void ICCF_ReloadPrefs() {
|
---|
276 | CFPreferencesAppSynchronize(kICBundleIdentifier);
|
---|
277 |
|
---|
278 | ICCF_prefs.commandClickEnabled = ICCF_GetBooleanPref(kICCommandClickEnabled, YES);
|
---|
279 | ICCF_prefs.textBlinkEnabled = ICCF_GetBooleanPref(kICTextBlinkEnabled, YES);
|
---|
280 | ICCF_prefs.textBlinkCount = ICCF_GetCFIndexPref(kICTextBlinkCount, 3);
|
---|
281 | ICCF_prefs.servicesInContextualMenu = ICCF_GetBooleanPref(kICServicesInContextualMenu, YES);
|
---|
282 | ICCF_prefs.servicesInMenuBar = ICCF_GetBooleanPref(kICServicesInMenuBar, NO);
|
---|
283 | ICCF_GetCFTypePref(kICServiceOptions, (CFTypeRef *)&ICCF_prefs.serviceOptions, CFDictionaryGetTypeID());
|
---|
284 | ICCF_prefs.terminalRequireOptionForSelfDrag = ICCF_GetBooleanPref(kICTerminalRequireOptionForSelfDrag, NO);
|
---|
285 | ICCF_prefs.errorSoundEnabled = ICCF_GetBooleanPref(kICErrorSoundEnabled, NO);
|
---|
286 | ICCF_prefs.errorDialogEnabled = ICCF_GetBooleanPref(kICErrorDialogEnabled, YES);
|
---|
287 |
|
---|
288 | CFPreferencesAppSynchronize(kICBundleIdentifier);
|
---|
289 |
|
---|
290 | ICCF_AddRemoveServicesMenu();
|
---|
291 | }
|
---|