Fixed the accessibility hierarchy of the message view to allow the accessibility...
[adiumx.git] / Source / AdiumContentFiltering.m
blob3d4f6140fc7c97452bb83c81d4187e0389801d01
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
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.
8  * 
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.
12  * 
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.
15  */
17 #import "AdiumContentFiltering.h"
19 @interface AdiumContentFiltering (PRIVATE)
20 - (void)_registerContentFilter:(id)inFilter
21                                    filterArray:(NSMutableArray *)inFilterArray;
22 @end
24 @implementation AdiumContentFiltering
26 /*!
27  * @brief Init
28  */
29 - (id)init
31         if((self = [super init])){
32                 stringsRequiringPolling = [[NSMutableSet alloc] init];
33                 delayedFilteringDict = [[NSMutableDictionary alloc] init];
34         }
35         
36         return self;
39 - (void)dealloc
40 {       
41         [stringsRequiringPolling release];
42         [delayedFilteringDict release];
44         [super dealloc];
48 //Content Filtering ----------------------------------------------------------------------------------------------------
49 #pragma mark Content Filtering
50 /*!
51  * @brief Register a content filter.
52  *
53  * If the particular filter wants to apply to multiple types or directions, it should register multiple times.
54  */
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];
64         }
65         
66         [self _registerContentFilter:inFilter
67                                          filterArray:contentFilter[type][direction]];
70 /*!
71  * @brief Register a delayed content filter
72  *
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.
76  */
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];
86         }
87         
88         //Register the filter
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];
95         }
96         [delayedContentFilters[type][direction] addObject:inFilter];
99 /*!
100  * @brief Unregister a filter.
102  * Looks in both contentFilter and delayedContentFilter, for all types and directions
103  */
104 - (void)unregisterContentFilter:(id<AIContentFilter>)inFilter
106         NSParameterAssert(inFilter != nil);
108         int i, j;
109         for (i = 0; i < FILTER_TYPE_COUNT; i++) {
110                 for (j = 0; j < FILTER_DIRECTION_COUNT; j++) {
111                         [contentFilter[i][j] removeObject:inFilter];
112                 }
113         }
117  * @brief Register a string to be filtered which requires polling to be updated
118  */
119 - (void)registerFilterStringWhichRequiresPolling:(NSString *)inPollString
121         [stringsRequiringPolling addObject:inPollString];
125  * @brief Is polling required to update the passed string?
126  */
127 - (BOOL)shouldPollToUpdateString:(NSString *)inString
129         NSEnumerator    *enumerator;
130         NSString                *stringRequiringPolling;
131         BOOL                    shouldPoll = NO;
132         
133         enumerator = [stringsRequiringPolling objectEnumerator];
134         while ((stringRequiringPolling = [enumerator nextObject])) {
135                 if ([inString rangeOfString:stringRequiringPolling].location != NSNotFound) {
136                         shouldPoll = YES;
137                         break;
138                 }
139         }
140         
141         return shouldPoll;
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
155  */
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];
164         id                              filter;
165         BOOL                    beganDelayedFiltering = NO;
166         NSMutableArray  *performedFilters;
167         
168         //If we're passed previouslyPerformedFilters, use them as a starting point for performedFilters
169         if (filtersToSkip) {
170                 performedFilters = [[filtersToSkip mutableCopy] autorelease];
171         } else {
172                 performedFilters = [NSMutableArray array];
173         }
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
181                                                                                                                                                                                                   uniqueID:uniqueID];                   
182                         } else {
183                                 *attributedString = [(id <AIContentFilter>)filter filterAttributedString:*attributedString context:filterContext];
184                         }
185                 }
186                 
187                 //Note that we've now completed this filter
188                 [performedFilters addObject:filter];
189         }
190         
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
206  */
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];
218         
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
226  * is returned.
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
236  */
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
243                                            context:(id)context
245         NSParameterAssert(type >= 0 && type < FILTER_TYPE_COUNT);
246         NSParameterAssert(direction >= 0 && direction < FILTER_DIRECTION_COUNT);
248         BOOL                            shouldDelay = NO;
249         NSInvocation            *invocation;
250         
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;
260                 
261                 //Perform the filters
262                 shouldDelay = [self _filterAttributedString:&attributedString
263                                                                           contentFilter:contentFilter[type][direction]
264                                                                           filterContext:filterContext
265                                                           uniqueDelayedFilterID:uniqueDelayedFilterID
266                                                                           filtersToSkip:nil
267                                                                         finishedFilters:&performedFilters];
269                 //If we should delay (a delayed filter is doing its thing), store what we need to finish later
270                 if (shouldDelay) {
271                         NSMutableDictionary *trackingDict;
272                         
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];
280                         
281                         if (performedFilters) {
282                                 [trackingDict setObject:performedFilters
283                                                                  forKey:@"Performed Filters"];
284                         }
285         
286                         //Track this so we can invoke with the filtered product later
287                         [delayedFilteringDict setObject:trackingDict
288                                                                          forKey:[NSNumber numberWithUnsignedLongLong:uniqueDelayedFilterID]];
289                 }
290                 
291                 //Increment our delayed filter ID
292                 uniqueDelayedFilterID++;
293         }
294         
295         //If we didn't delay, invoke immediately
296         if (!shouldDelay) {
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];
299                 
300                 //Send the filtered attributedString back via the invocation
301                 [invocation invoke];
302         }
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.
313  */
314 - (void)delayedFilterDidFinish:(NSAttributedString *)attributedString uniqueID:(unsigned long long)uniqueID
316         NSNumber                        *uniqueIDNumber;
317         NSMutableDictionary     *infoDict;
318         NSArray                         *performedFilters = nil;
319         BOOL                            shouldDelay;
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];
331         
332         //If we no longer need to delay, set up the invocation and invoke it
333         if (!shouldDelay) {
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
340                 [invocation invoke];
342                 //No further need for the infoDict from delayedFilteringDict
343                 [delayedFilteringDict removeObjectForKey:uniqueIDNumber];
345         } else {
346                 /* performedFilters may now be a different object after filters ran;
347                  * update the infoDict for the next delayedFilterDidFinsh:uniqueId: call
348                  */
349                 [infoDict setObject:performedFilters
350                                          forKey:@"Performed Filters"];
351         }
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];
359         
360         if (filterPriorityA < filterPriorityB)
361                 return NSOrderedAscending;
362         else if (filterPriorityA > filterPriorityB)
363                 return NSOrderedDescending;
364         else
365                 return NSOrderedSame;
369  * @brief Add a content filter to the specified array
371  * Adds, then sorts by priority
372  */
373 - (void)_registerContentFilter:(id)inFilter
374                                    filterArray:(NSMutableArray *)inFilterArray
376         NSParameterAssert(inFilter != nil);
377         
378         [inFilterArray addObject:inFilter];
379         [inFilterArray sortUsingFunction:filterSort context:nil];       
382 @end