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

Last change on this file since 388 was 388, checked in by Nicholas Riley, 12 years ago

APEMain.m: Note missing Xcode 3 support. Add Terminal 2.0 support.

English.lproj/APE Manager plugin.nib:

English.lproj/APEInfo.rtfd:

ICeCoffEE.[hm]: Bring triggering window and app to front if error
dialog displayed in ICCF_HandleException. Remove initial word
selection. Launch preexisting selection if present. Remove 10.3
support. Update for new ICeCoffEETrigger API. Rename downEvent
parameter for consistency.

ICeCoffEE.xcodeproj: Added files.

ICeCoffEEParser.m: Handle multiline URLs.

ICeCoffEETTView.[hm]: Terminal 2.0 support (thank you for having a
usable NSTextInput implementation!)

ICeCoffEETTViewTrigger.[hm]: ICeCoffEETrigger implementation for
TTView. Can't set range as we do for NSTextView because you can't
have an arbitrary empty selection range in Terminal, so we pass it
directly to ICCF_LaunchURLFromTTView.

ICeCoffEETerminal.[hm]: Fix capitalization on ICeCoffEETermSubviewSuper.
Pass downEvent to ICCF_HandleException so it can bring the triggering
window to the front.

ICeCoffEETextViewTrigger.[hm]: Moved from ICeCoffEETrigger.

ICeCoffEETrigger.[hm]: Now an abstract superclass. Replace direct
access to ICCF_sharedTrigger with +cancel. Create 0-character range
(if click outside selectedRange) or save selectedRange. Move
debugging logs here from ICeCoffEE.m. Add description method.

ICeCoffEEWebKit.m: Pass downEvent to ICCF_HandleException so it can
bring the triggering window to the front.

Installer components/ui/ui.plist: Updated for 1.5d3.

TestParser?.m: Handle multiline URLs; newlines are printed as \ and
tabs as >. Implement ICCF_StringByRemovingCharactersInSet, which will
move elsewhere once Internet Config support is removed.

VERSION.xcconfig: Updated for 1.5d3.

urls.plist: Test for multiline URLs.

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