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;
23 - (NSAttributedString *)_filterAttributedString:(NSAttributedString *)attributedString
24 contentFilter:(NSArray *)inContentFilterArray
25 filterContext:(id)filterContext;
27 int filterSort(id<AIContentFilter> filterA, id<AIContentFilter> filterB, void *context);
31 @implementation AdiumContentFiltering
38 if((self = [super init])){
39 stringsRequiringPolling = [[NSMutableSet alloc] init];
40 delayedFilteringDict = [[NSMutableDictionary alloc] init];
55 //Content Filtering ----------------------------------------------------------------------------------------------------
56 #pragma mark Content Filtering
58 * @brief Register a content filter.
60 * If the particular filter wants to apply to multiple types or directions, it should register multiple times.
62 - (void)registerContentFilter:(id<AIContentFilter>)inFilter
63 ofType:(AIFilterType)type
64 direction:(AIFilterDirection)direction
66 NSParameterAssert(type >= 0 && type < FILTER_TYPE_COUNT);
67 NSParameterAssert(direction >= 0 && direction < FILTER_DIRECTION_COUNT);
69 if (!contentFilter[type][direction]) {
70 contentFilter[type][direction] = [[NSMutableArray alloc] init];
73 [self _registerContentFilter:inFilter
74 filterArray:contentFilter[type][direction]];
78 * @brief Register a delayed content filter
80 * Delayed content filters return YES or NO from their filter method; YES means they began a filtering process.
81 * When finished, the filter is responsible for notifying this class that the attributed string is ready.
82 * A unique ID will be passed to identify each string.
84 - (void)registerDelayedContentFilter:(id<AIDelayedContentFilter>)inFilter
85 ofType:(AIFilterType)type
86 direction:(AIFilterDirection)direction
88 NSParameterAssert(type >= 0 && type < FILTER_TYPE_COUNT);
89 NSParameterAssert(direction >= 0 && direction < FILTER_DIRECTION_COUNT);
91 if (!delayedContentFilter[type][direction]) {
92 delayedContentFilter[type][direction] = [[NSMutableArray alloc] init];
95 [self _registerContentFilter:inFilter
96 filterArray:delayedContentFilter[type][direction]];
100 * @brief Add a content filter to the specified array
102 * Adds, then sorts by priority
104 - (void)_registerContentFilter:(id)inFilter
105 filterArray:(NSMutableArray *)inFilterArray
107 NSParameterAssert(inFilter != nil);
109 [inFilterArray addObject:inFilter];
110 [inFilterArray sortUsingFunction:filterSort context:nil];
114 * @brief Unregister a filter.
116 * Looks in both contentFilter and delayedContentFilter, for all types and directions
118 - (void)unregisterContentFilter:(id<AIContentFilter>)inFilter
120 NSParameterAssert(inFilter != nil);
123 for (i = 0; i < FILTER_TYPE_COUNT; i++) {
124 for (j = 0; j < FILTER_DIRECTION_COUNT; j++) {
125 [contentFilter[i][j] removeObject:inFilter];
126 [delayedContentFilter[i][j] removeObject:inFilter];
132 * @brief Register a string to be filtered which requires polling to be updated
134 - (void)registerFilterStringWhichRequiresPolling:(NSString *)inPollString
136 [stringsRequiringPolling addObject:inPollString];
140 * @brief Is polling required to update the passed string?
142 - (BOOL)shouldPollToUpdateString:(NSString *)inString
144 NSEnumerator *enumerator;
145 NSString *stringRequiringPolling;
146 BOOL shouldPoll = NO;
148 enumerator = [stringsRequiringPolling objectEnumerator];
149 while ((stringRequiringPolling = [enumerator nextObject])) {
150 if ([inString rangeOfString:stringRequiringPolling].location != NSNotFound) {
160 * @brief Filter an attributed string immediately
162 * This does not perform delayed filters.
164 * @param attributedString NSAttributedString to filter
165 * @param type Type of the filter
166 * @param direction Direction of the filter
167 * @param filterContext A object, such as an AIListContact or an AIAccount, used as context by filters
168 * @result The filtered attributed string, which may be the same as attributedString
170 - (NSAttributedString *)filterAttributedString:(NSAttributedString *)attributedString
171 usingFilterType:(AIFilterType)type
172 direction:(AIFilterDirection)direction
173 context:(id)filterContext
175 attributedString = [self _filterAttributedString:attributedString
176 contentFilter:contentFilter[type][direction]
177 filterContext:filterContext];
179 return attributedString;
184 * @brief Perform the filtering of an attributedString on the specified content filter.
186 * @param attributedString NSAttributedString to filter
187 * @param inContentFilterArray Array of filters to use
188 * @param filtercontext Passed to each filter as context.
189 * @result The filtered NSAttributedString, which may be the same as attributedString
191 - (NSAttributedString *)_filterAttributedString:(NSAttributedString *)attributedString
192 contentFilter:(NSArray *)inContentFilterArray
193 filterContext:(id)filterContext
195 NSEnumerator *enumerator = [inContentFilterArray objectEnumerator];
196 id<AIContentFilter> filter;
198 while ((filter = [enumerator nextObject])) {
199 attributedString = [filter filterAttributedString:attributedString context:filterContext];
202 return attributedString;
206 * @brief Begin delayed filtering of an attributedString
208 * @result YES if any delayed filtering began; NO if it did not
210 - (BOOL)_delayedFilterAttributedString:(NSAttributedString *)attributedString
211 contentFilter:(NSArray *)inContentFilterArray
212 filterContext:(id)filterContext
213 uniqueDelayedFilterID:(unsigned long long)uniqueID
215 NSEnumerator *enumerator = [inContentFilterArray objectEnumerator];
216 id<AIDelayedContentFilter> filter;
217 BOOL beganDelayedFiltering = NO;
219 //Break as soon as we begin delayed filtering; we'll be back through here when that filtering is done
220 while ((filter = [enumerator nextObject]) && !beganDelayedFiltering) {
221 beganDelayedFiltering = [filter delayedFilterAttributedString:attributedString
222 context:filterContext
226 return beganDelayedFiltering;
230 * @brief Filter an attributed string, notifying a target when complete
232 * This performs delayed filters, which means there may be a non-blocking delay before the filtered attributed string
235 * @param attributedString NSAttributedString to filter
236 * @param type Type of the filter
237 * @param direction Direction of the filter
238 * @param filterContext A object, such as an AIListContact or an AIAccount, used as context by filters
239 * @param target Target to notify when filtering is complete
240 * @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.
241 * @param context Context passed back to target via selector when filtering is complete
242 * @result The filtered attributed string, which may be the same as attributedString
244 - (void)filterAttributedString:(NSAttributedString *)attributedString
245 usingFilterType:(AIFilterType)type
246 direction:(AIFilterDirection)direction
247 filterContext:(id)filterContext
248 notifyingTarget:(id)target
249 selector:(SEL)selector
252 NSParameterAssert(type >= 0 && type < FILTER_TYPE_COUNT);
253 NSParameterAssert(direction >= 0 && direction < FILTER_DIRECTION_COUNT);
255 BOOL shouldDelay = NO;
256 NSInvocation *invocation;
258 //Set up the invocation
259 invocation = [NSInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]];
260 [invocation setSelector:selector];
261 [invocation setTarget:target];
262 [invocation setArgument:&context atIndex:3]; //context, the second argument after the two hidden arguments of every NSInvocation
264 if (attributedString) {
265 static unsigned long long uniqueDelayedFilterID = 0;
267 //Perform the main filters
268 attributedString = [self _filterAttributedString:attributedString
269 contentFilter:contentFilter[type][direction]
270 filterContext:filterContext];
272 //Now perform the delayed filters
273 shouldDelay = [self _delayedFilterAttributedString:attributedString
274 contentFilter:delayedContentFilter[type][direction]
275 filterContext:filterContext
276 uniqueDelayedFilterID:uniqueDelayedFilterID];
278 //If we should delay (a delayed filter is doing its thing), store what we need to finish later
280 //NSInvocation does not retain its arguments by default; if we're caching the invocation, we must tell it to.
281 [invocation retainArguments];
283 //Track this so we can invoke with the filtered product later
284 [delayedFilteringDict setObject:[NSDictionary dictionaryWithObjectsAndKeys:
285 invocation, @"Invocation",
286 delayedContentFilter[type][direction], @"Delayed Content Filter",
287 filterContext, @"Filter Context", nil]
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 NSDictionary *infoDict;
320 uniqueIDNumber = [NSNumber numberWithUnsignedLongLong:uniqueID];
321 infoDict = [delayedFilteringDict objectForKey:uniqueIDNumber];
323 //Run through the delayed filters again, since a delayed filter would stop after the first hit
324 shouldDelay = [self _delayedFilterAttributedString:attributedString
325 contentFilter:[infoDict objectForKey:@"Delayed Content Filter"]
326 filterContext:[infoDict objectForKey:@"Filter Context"]
327 uniqueDelayedFilterID:uniqueID];
329 //If we no longer need to delay, set up the invocation and invoke it
331 NSInvocation *invocation = [infoDict objectForKey:@"Invocation"];
333 //Put that attributed string into the invocation as the first argument after the two hidden arguments of every NSInvocation
334 [invocation setArgument:&attributedString atIndex:2];
336 //Send the filtered attributedString back via the invocation
339 //No further need for the infoDict from delayedFilteringDict
340 [delayedFilteringDict removeObjectForKey:uniqueIDNumber];
344 #pragma mark Filter priority sort
345 int filterSort(id<AIContentFilter> filterA, id<AIContentFilter> filterB, void *context)
347 float filterPriorityA = [filterA filterPriority];
348 float filterPriorityB = [filterB filterPriority];
350 if (filterPriorityA < filterPriorityB)
351 return NSOrderedAscending;
352 else if (filterPriorityA > filterPriorityB)
353 return NSOrderedDescending;
355 return NSOrderedSame;