2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AdiumContentFiltering.h"
19 @interface AdiumContentFiltering (PRIVATE)
20 - (void)_registerContentFilter:(id)inFilter
21 filterArray:(NSMutableArray *)inFilterArray;
24 @implementation AdiumContentFiltering
31 if((self = [super init])){
32 stringsRequiringPolling = [[NSMutableSet alloc] init];
33 delayedFilteringDict = [[NSMutableDictionary alloc] init];
41 [stringsRequiringPolling release];
42 [delayedFilteringDict release];
48 //Content Filtering ----------------------------------------------------------------------------------------------------
49 #pragma mark Content Filtering
51 * @brief Register a content filter.
53 * If the particular filter wants to apply to multiple types or directions, it should register multiple times.
55 - (void)registerContentFilter:(id<AIContentFilter>)inFilter
56 ofType:(AIFilterType)type
57 direction:(AIFilterDirection)direction
59 NSParameterAssert(type >= 0 && type < FILTER_TYPE_COUNT);
60 NSParameterAssert(direction >= 0 && direction < FILTER_DIRECTION_COUNT);
62 if (!contentFilter[type][direction]) {
63 contentFilter[type][direction] = [[NSMutableArray alloc] init];
66 [self _registerContentFilter:inFilter
67 filterArray:contentFilter[type][direction]];
71 * @brief Register a delayed content filter
73 * Delayed content filters return YES or NO from their filter method; YES means they began a filtering process.
74 * When finished, the filter is responsible for notifying this class that the attributed string is ready.
75 * A unique ID will be passed to identify each string.
77 - (void)registerDelayedContentFilter:(id<AIDelayedContentFilter>)inFilter
78 ofType:(AIFilterType)type
79 direction:(AIFilterDirection)direction
81 NSParameterAssert(type >= 0 && type < FILTER_TYPE_COUNT);
82 NSParameterAssert(direction >= 0 && direction < FILTER_DIRECTION_COUNT);
84 if (!contentFilter[type][direction]) {
85 contentFilter[type][direction] = [[NSMutableArray alloc] init];
89 [self _registerContentFilter:inFilter
90 filterArray:contentFilter[type][direction]];
92 //Note that this is a delayed filter
93 if (!delayedContentFilters[type][direction]) {
94 delayedContentFilters[type][direction] = [[NSMutableArray alloc] init];
96 [delayedContentFilters[type][direction] addObject:inFilter];
100 * @brief Unregister a filter.
102 * Looks in both contentFilter and delayedContentFilter, for all types and directions
104 - (void)unregisterContentFilter:(id<AIContentFilter>)inFilter
106 NSParameterAssert(inFilter != nil);
109 for (i = 0; i < FILTER_TYPE_COUNT; i++) {
110 for (j = 0; j < FILTER_DIRECTION_COUNT; j++) {
111 [contentFilter[i][j] removeObject:inFilter];
117 * @brief Register a string to be filtered which requires polling to be updated
119 - (void)registerFilterStringWhichRequiresPolling:(NSString *)inPollString
121 [stringsRequiringPolling addObject:inPollString];
125 * @brief Is polling required to update the passed string?
127 - (BOOL)shouldPollToUpdateString:(NSString *)inString
129 NSEnumerator *enumerator;
130 NSString *stringRequiringPolling;
131 BOOL shouldPoll = NO;
133 enumerator = [stringsRequiringPolling objectEnumerator];
134 while ((stringRequiringPolling = [enumerator nextObject])) {
135 if ([inString rangeOfString:stringRequiringPolling].location != NSNotFound) {
145 * @brief Perform the filtering of an attributedString on the specified content filter.
147 * @param attributedString A pointer to the NSAttributedString to filter
148 * @param inContentFilterArray Array of filters to use, which must each conform to either AIDelayedContentFilter or AIContentFilter
149 * @param filterContext Passed to each filter as context.
150 * @param uniqueID A unique ID used by delayed filters
151 * @param filtersToSkip An array of filters which should not be performed, such as previously performed or inappropriate filters
152 * @param finishedFilters A pointer to an array which will be filled with the filters which were performed, suitable for passing later as performedFilters
154 * @result YES if any delayed filtering began; NO if it did not
156 - (BOOL)_filterAttributedString:(NSAttributedString **)attributedString
157 contentFilter:(NSArray *)inContentFilterArray
158 filterContext:(id)filterContext
159 uniqueDelayedFilterID:(unsigned long long)uniqueID
160 filtersToSkip:(NSArray *)filtersToSkip
161 finishedFilters:(NSArray **)finishedFilters
163 NSEnumerator *enumerator = [inContentFilterArray objectEnumerator];
165 BOOL beganDelayedFiltering = NO;
166 NSMutableArray *performedFilters;
168 //If we're passed previouslyPerformedFilters, use them as a starting point for performedFilters
170 performedFilters = [[filtersToSkip mutableCopy] autorelease];
172 performedFilters = [NSMutableArray array];
175 while ((filter = [enumerator nextObject]) && !beganDelayedFiltering) {
176 //Only run the filter if there were no previously performed filters or this hasn't been previously done
177 if (!filtersToSkip || ![filtersToSkip containsObject:filter]) {
178 if ([filter conformsToProtocol:@protocol(AIDelayedContentFilter)]) {
179 beganDelayedFiltering = [(id <AIDelayedContentFilter>)filter delayedFilterAttributedString:*attributedString
180 context:filterContext
183 *attributedString = [(id <AIContentFilter>)filter filterAttributedString:*attributedString context:filterContext];
187 //Note that we've now completed this filter
188 [performedFilters addObject:filter];
191 if (finishedFilters) *finishedFilters = performedFilters;
193 return beganDelayedFiltering;
197 * @brief Filter an attributed string immediately
199 * This does not perform delayed filters (it passes the delayed content filters as filtersToSkip).
201 * @param attributedString NSAttributedString to filter
202 * @param type Type of the filter
203 * @param direction Direction of the filter
204 * @param filterContext A object, such as an AIListContact or an AIAccount, used as context by filters
205 * @result The filtered attributed string, which may be the same as attributedString
207 - (NSAttributedString *)filterAttributedString:(NSAttributedString *)attributedString
208 usingFilterType:(AIFilterType)type
209 direction:(AIFilterDirection)direction
210 context:(id)filterContext
212 [self _filterAttributedString:&attributedString
213 contentFilter:contentFilter[type][direction]
214 filterContext:filterContext
215 uniqueDelayedFilterID:0
216 filtersToSkip:delayedContentFilters[type][direction]
217 finishedFilters:NULL];
219 return attributedString;
223 * @brief Filter an attributed string, notifying a target when complete
225 * This performs delayed filters, which means there may be a non-blocking delay before the filtered attributed string
228 * @param attributedString NSAttributedString to filter
229 * @param type Type of the filter
230 * @param direction Direction of the filter
231 * @param filterContext A object, such as an AIListContact or an AIAccount, used as context by filters
232 * @param target Target to notify when filtering is complete
233 * @param selector Selector to call on target. It should take 2 arguments; the first will be the filtered attributedString; the second is the passed context.
234 * @param context Context passed back to target via selector when filtering is complete
235 * @result The filtered attributed string, which may be the same as attributedString
237 - (void)filterAttributedString:(NSAttributedString *)attributedString
238 usingFilterType:(AIFilterType)type
239 direction:(AIFilterDirection)direction
240 filterContext:(id)filterContext
241 notifyingTarget:(id)target
242 selector:(SEL)selector
245 NSParameterAssert(type >= 0 && type < FILTER_TYPE_COUNT);
246 NSParameterAssert(direction >= 0 && direction < FILTER_DIRECTION_COUNT);
248 BOOL shouldDelay = NO;
249 NSInvocation *invocation;
251 //Set up the invocation
252 invocation = [NSInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]];
253 [invocation setSelector:selector];
254 [invocation setTarget:target];
255 [invocation setArgument:&context atIndex:3]; //context, the second argument after the two hidden arguments of every NSInvocation
257 if (attributedString) {
258 static unsigned long long uniqueDelayedFilterID = 0;
259 NSArray *performedFilters = nil;
261 //Perform the filters
262 shouldDelay = [self _filterAttributedString:&attributedString
263 contentFilter:contentFilter[type][direction]
264 filterContext:filterContext
265 uniqueDelayedFilterID:uniqueDelayedFilterID
267 finishedFilters:&performedFilters];
269 //If we should delay (a delayed filter is doing its thing), store what we need to finish later
271 NSMutableDictionary *trackingDict;
273 //NSInvocation does not retain its arguments by default; if we're caching the invocation, we must tell it to.
274 [invocation retainArguments];
276 trackingDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
277 invocation, @"Invocation",
278 contentFilter[type][direction], @"Delayed Content Filter",
279 filterContext, @"Filter Context", nil];
281 if (performedFilters) {
282 [trackingDict setObject:performedFilters
283 forKey:@"Performed Filters"];
286 //Track this so we can invoke with the filtered product later
287 [delayedFilteringDict setObject:trackingDict
288 forKey:[NSNumber numberWithUnsignedLongLong:uniqueDelayedFilterID]];
291 //Increment our delayed filter ID
292 uniqueDelayedFilterID++;
295 //If we didn't delay, invoke immediately
297 //Put that attributed string into the invocation as the first argument after the two hidden arguments of every NSInvocation
298 [invocation setArgument:&attributedString atIndex:2];
300 //Send the filtered attributedString back via the invocation
306 * @brief A delayed filter finished filtering
308 * After this filter finishes, run it through the delayed filter system again
309 * to hit the next delayed string, if necessary.
311 * If no more delayed filtering is needed, look up the invocation and pass the
312 * now-finished string to the appropriate target.
314 - (void)delayedFilterDidFinish:(NSAttributedString *)attributedString uniqueID:(unsigned long long)uniqueID
316 NSNumber *uniqueIDNumber;
317 NSMutableDictionary *infoDict;
318 NSArray *performedFilters = nil;
321 uniqueIDNumber = [NSNumber numberWithUnsignedLongLong:uniqueID];
322 infoDict = [delayedFilteringDict objectForKey:uniqueIDNumber];
324 //Run through the filters again, skipping the ones we did previously, since a delayed filter would stop after the first hit
325 shouldDelay = [self _filterAttributedString:&attributedString
326 contentFilter:[infoDict objectForKey:@"Delayed Content Filter"]
327 filterContext:[infoDict objectForKey:@"Filter Context"]
328 uniqueDelayedFilterID:uniqueID
329 filtersToSkip:[infoDict objectForKey:@"Performed Filters"]
330 finishedFilters:&performedFilters];
332 //If we no longer need to delay, set up the invocation and invoke it
334 NSInvocation *invocation = [infoDict objectForKey:@"Invocation"];
336 //Put that attributed string into the invocation as the first argument after the two hidden arguments of every NSInvocation
337 [invocation setArgument:&attributedString atIndex:2];
339 //Send the filtered attributedString back via the invocation
342 //No further need for the infoDict from delayedFilteringDict
343 [delayedFilteringDict removeObjectForKey:uniqueIDNumber];
346 /* performedFilters may now be a different object after filters ran;
347 * update the infoDict for the next delayedFilterDidFinsh:uniqueId: call
349 [infoDict setObject:performedFilters
350 forKey:@"Performed Filters"];
354 #pragma mark Filter priority sort
355 static int filterSort(id<AIContentFilter> filterA, id<AIContentFilter> filterB, void *context)
357 float filterPriorityA = [filterA filterPriority];
358 float filterPriorityB = [filterB filterPriority];
360 if (filterPriorityA < filterPriorityB)
361 return NSOrderedAscending;
362 else if (filterPriorityA > filterPriorityB)
363 return NSOrderedDescending;
365 return NSOrderedSame;
369 * @brief Add a content filter to the specified array
371 * Adds, then sorts by priority
373 - (void)_registerContentFilter:(id)inFilter
374 filterArray:(NSMutableArray *)inFilterArray
376 NSParameterAssert(inFilter != nil);
378 [inFilterArray addObject:inFilter];
379 [inFilterArray sortUsingFunction:filterSort context:nil];