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

Last change on this file since 183 was 183, checked in by Nicholas Riley, 15 years ago

ICeCoffEE 1.4.2b1

VERSION, ui.plist, Info*.plist, InfoPlist?.strings: Updated for 1.4.2b1.

APEMain.m: Removed PBX support.

ICeCoffEE.[hm]: Don't ICCF_OSErr(C)Assert if we get userCanceledErr:
part of fixing extraneous exception when not selecting anything from
the helper app menu. ICCF_KeyboardAction() and
ICCF_LaunchURLFromTextView() now take an event parameter - since we
have stuff on a timer now, means we get the key modifier state at
mousedown time, rather than some arbitrary time later.
ICCF_LaunchURL() returns NO if the user cancelled, so we don't need to
throw an exception to stop the URL blinking. Moved sanitized mouse
down event generation to ICCF_MouseDownEventWithModifierFlags(). Only
update Services menu at app launch on Panther. Use a timer to delay
URL launching in NSTextView on Tiger, so we accommodate
command-multiple clicking for discontiguous selection. Remove that
ugly goto.

ICeCoffEEShared.h: Turn off debugging in preparation for (beta)
release.

ICeCoffEETerminal.m: Update for new ICCF_LaunchURL() return.

ICeCoffEETrigger.[hm]: Singleton timer wrapper for discontiguous
selection compatibility on Tiger. Singleton global is exported for
efficiency since we have to check it on every mouse down.

ICeCoffEEWebKit.m: Removed incorrect comment (what was I thinking?)
Update for new ICCF_LaunchURL() return. Properly highlight before
ICCF_LaunchURL(), especially noticable with menu.

APEInfo.rtfd: More fixes and updates, final for 1.4.2b1.

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