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

Last change on this file since 88 was 88, checked in by Nicholas Riley, 19 years ago

ICeCoffEE 1.3.1d1

File size: 11.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#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
69void APEBundleMain(CFBundleRef inBundle)
70{
71    ICCF_MigratePrefs();
72
73    // first check if this application is in the exclude list;
74    // if it is, simply return and do not apply any patches.
75    // APETools will help us with that; APE Manager will take care of
76    // exclude list management for us.
77    if (APEToolsIsInExcludeList(kICBundleIdentifier, NULL))
78    {
79        ICapeprintf("ICeCoffEE APE: not loading as this application is excluded.\n");
80        return;
81    }
82
83    CPSProcessSerNum psn;
84    OSStatus err = CPSGetCurrentProcess(&psn);
85    if (err != noErr) {
86        apeprintf("ICeCoffEE APE: Can't get process serial number for current process (error %ld); not loading in this application\n", err);
87        return;
88    }
89   
90    CPSProcessInfoRec info;
91    err = CPSGetProcessInfo(&psn, &info, NULL, 0, NULL, NULL, 0);
92    if (err != noErr) {
93        apeprintf("ICeCoffEE APE: Can't get process information (error %ld); not loading in this application\n", err);
94        return;
95    }
96    ICapeprintf("ICeCoffEE APE: got process attributes = 0x%lx\n", info.Attributes);
97    if (info.Attributes & (kCPSBGOnlyAttr | kCPSUIElementAttr | kCPSFullScreenAttr)) {
98        ICapeprintf("ICeCoffEE APE: not loading as this application is background-only\n");
99        return;
100    }
101
102    ICCF_bundle = CFBundleGetBundleWithIdentifier(kICBundleIdentifier);
103    CFStringRef bundleID = CFBundleGetIdentifier(CFBundleGetMainBundle());
104    BOOL shouldLoadInNSTextView = YES;
105
106    // XXX handle patching error return from ICCF_PatchMethod
107    if (bundleID != NULL) {
108        if (CFStringCompare(bundleID, CFSTR("com.apple.projectbuilder"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
109            ICCF_PatchMethod("PBXTextView", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:");
110            ICapeprintf("ICeCoffEE APE: loaded in PBXTextView for PB!\n");
111            shouldLoadInNSTextView = NO;
112        } else if (CFStringCompare(bundleID, CFSTR("com.apple.terminal"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
113            ICCF_PatchMethod("TermSubview", "ICeCoffEEMenuOnly", "ICeCoffEEMenuSuper", "menuForEvent:") &&
114            ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "selectedRange") &&
115            ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "attributedSubstringFromRange:") &&
116            ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "mouseDown:") &&
117            ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "mouseUp:") &&
118            ICCF_PatchMethod("TermSubview", "ICeCoffEETerminal", "ICECoffEETermSubviewSuper", "_optionClickEvent:::") ,
119            ICapeprintf("ICeCoffEE APE: loaded in TermSubview for Terminal!\n");
120        } else if (CFStringCompare(bundleID, CFSTR("com.apple.safari"), kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
121            ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "mouseUp:") &&
122            ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "mouseDown:") &&
123            ICCF_PatchMethod("WebHTMLView", "ICeCoffEEWebKit", "ICeCoffEEWebKitSuper", "menuForEvent:") ,
124            ICapeprintf("ICeCoffEE APE: loaded in WebHTMLView for Safari!\n");
125        }
126    }
127
128    if (shouldLoadInNSTextView) {
129        ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "mouseDown:") &&
130        ICCF_PatchMethod("NSTextView", "ICeCoffEE", "ICeCoffEESuper", "menuForEvent:") ,
131        ICapeprintf("ICeCoffEE APE: loaded generic NSTextView support\n");
132    }
133
134    gTEClick = APEPatchCreate(&TEClick, &ICCF_TEClick);
135    if (gTEClick != NULL) {
136        ICapeprintf("ICeCoffEE APE: patched TEClick\n");
137    }
138
139    ICCF_ReloadPrefs();
140       
141    return;
142}
143
144// 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.
145OSStatus APEBundleMessage(CFStringRef message,CFDataRef inData,CFDataRef *outData)
146{
147    ICapeprintf("ICeCoffEE APE: message '%@' (inData = %@)\n", message, inData);
148   
149    if (CFStringCompare(message, kICPreferencesChanged, NULL) == kCFCompareEqualTo)             
150    {   // request to reload prefs from our preference pane
151        ICCF_ReloadPrefs();
152    }
153   
154    return noErr;
155}
156
157Boolean ICCF_GetBooleanPref(CFStringRef prefKey, Boolean defaultValue) {
158    Boolean keyExists;
159    Boolean value = CFPreferencesGetAppBooleanValue(prefKey, kICBundleIdentifier, &keyExists);
160    if (keyExists) return value;
161    CFPreferencesSetAppValue(prefKey, defaultValue ? kCFBooleanTrue : kCFBooleanFalse, kICBundleIdentifier);
162    return defaultValue;
163}
164
165CFIndex ICCF_GetCFIndexPref(CFStringRef prefKey, CFIndex defaultValue) {
166    Boolean keyExists;
167    CFIndex value = CFPreferencesGetAppIntegerValue(prefKey, kICBundleIdentifier, &keyExists);
168    if (keyExists) return value;
169    CFNumberRef defaultValueNumber = CFNumberCreate(NULL, kCFNumberCFIndexType, &defaultValue);
170    CFPreferencesSetAppValue(prefKey, defaultValueNumber, kICBundleIdentifier);
171    CFRelease(defaultValueNumber);
172    return defaultValue;
173}
174
175// to test: defaults write net.sabi.ICeCoffEE "Excluded Applications" -array '{CFBundleID = "com.apple.projectbuilder"; }' '{CFBundleID = "net.sabi.Pester"; }' '{CFBundleID = "com.apple.foobar"; }'
176static void ICCF_MigratePrefs() {
177   
178    CFArrayRef prefExcludedApps = CFPreferencesCopyAppValue(kIC12PrefExcluded, kICBundleIdentifier);
179
180    if (prefExcludedApps == NULL) return;
181
182    if (CFGetTypeID(prefExcludedApps) != CFArrayGetTypeID()) {
183        CFRelease(prefExcludedApps);
184        return;
185    }
186   
187    CFMutableArrayRef excludedApps = CFArrayCreateMutableCopy(NULL, 0, prefExcludedApps);
188    CFRelease(prefExcludedApps); prefExcludedApps = NULL;
189
190    ICapeprintf("Excluded apps: %@\n", excludedApps);
191
192    CFIndex excludedAppCount = CFArrayGetCount(excludedApps);
193    CFIndex excludedAppIndex;
194
195    CFDictionaryRef excludedAppSpecifiers = NULL;
196    CFStringRef excludedAppBundleID = NULL;
197    CFURLRef excludedAppURL = NULL;
198    CFBundleRef excludedAppBundle = NULL;
199
200    BOOL postponeMigrationForApp;
201
202    for (excludedAppIndex = excludedAppCount - 1 ; excludedAppIndex >= 0 ; excludedAppIndex--) {
203        if ( (excludedAppSpecifiers = CFArrayGetValueAtIndex(excludedApps, excludedAppIndex)) == NULL || CFGetTypeID(excludedAppSpecifiers) != CFDictionaryGetTypeID()) {
204            CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
205            continue;
206        }
207
208        postponeMigrationForApp = NO;
209        if ( (excludedAppBundleID = CFDictionaryGetValue(excludedAppSpecifiers, kIC12PrefExcludedAppSpecifierBundleID)) != NULL
210             && CFGetTypeID(excludedAppBundleID) == CFStringGetTypeID()
211             && CFStringCompare(excludedAppBundleID, CFSTR("com.apple.projectbuilder"), kCFCompareCaseInsensitive) != kCFCompareEqualTo) {
212            if (LSFindApplicationForInfo(kLSUnknownCreator, excludedAppBundleID, NULL, NULL, &excludedAppURL) == noErr) {
213                excludedAppBundle = CFBundleCreate(NULL, excludedAppURL);
214                if (excludedAppBundle != NULL) {
215                    APEToolsAddToExcludeList(kICBundleIdentifier, excludedAppBundle);
216                } else postponeMigrationForApp = YES; // can't create bundle
217            } else postponeMigrationForApp = YES; // can't find app
218        }
219        // don't release excludedAppSpecifiers, used Get
220        // don't release excludedAppBundleID, used Get
221        if (excludedAppURL != NULL) { CFRelease(excludedAppURL); excludedAppURL = NULL; }
222        if (excludedAppBundle != NULL) { CFRelease(excludedAppBundle); excludedAppBundle = NULL; }
223        if (!postponeMigrationForApp) CFArrayRemoveValueAtIndex(excludedApps, excludedAppIndex);
224    }
225
226    ICapeprintf("Excluded apps remaining: %@\n", excludedApps);
227    CFPreferencesSetAppValue(kIC12PrefExcluded, (CFArrayGetCount(excludedApps) == 0 ? NULL : excludedApps), kICBundleIdentifier);
228    CFRelease(excludedApps);
229
230    CFPreferencesAppSynchronize(kICBundleIdentifier);
231}
232
233static void ICCF_ReloadPrefs() {
234    CFPreferencesAppSynchronize(kICBundleIdentifier);
235
236    ICCF_prefs.commandClickEnabled = ICCF_GetBooleanPref(kICCommandClickEnabled, YES);
237    ICCF_prefs.textBlinkEnabled = ICCF_GetBooleanPref(kICTextBlinkEnabled, YES);
238    ICCF_prefs.textBlinkCount = ICCF_GetCFIndexPref(kICTextBlinkCount, 3);
239    ICCF_prefs.servicesInContextualMenu = ICCF_GetBooleanPref(kICServicesInContextualMenu, YES);
240    ICCF_prefs.servicesInMenuBar = ICCF_GetBooleanPref(kICServicesInMenuBar, NO);
241    ICCF_prefs.errorSoundEnabled = ICCF_GetBooleanPref(kICErrorSoundEnabled, NO);
242    ICCF_prefs.errorDialogEnabled = ICCF_GetBooleanPref(kICErrorDialogEnabled, YES);
243
244    CFPreferencesAppSynchronize(kICBundleIdentifier);
245
246    ICCF_AddRemoveServicesMenu();
247}
Note: See TracBrowser for help on using the repository browser.