Unescape the HREF attribute's text before passing it to NSURL which does not expect...
[adiumx.git] / Source / AIEmoticonController.m
blob30cc1cb3c4d4597382943209357d530a483bbd6c
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 "AIEmoticonController.h"
18 #import "AIEmoticon.h"
19 #import "AIEmoticonPack.h"
20 #import "AIEmoticonPreferences.h"
21 #import <Adium/AIContentObject.h>
22 #import <Adium/AIContentMessage.h>
23 #import <Adium/AIAccountControllerProtocol.h>
24 #import <Adium/AIContentControllerProtocol.h>
25 #import <Adium/AIPreferenceControllerProtocol.h>
26 #import <Adium/AIAccount.h>
27 #import <Adium/AIListObject.h>
28 #import <Adium/AIListContact.h>
29 #import <Adium/AIService.h>
30 #import <AIUtilities/AIDictionaryAdditions.h>
31 #import <AIUtilities/AICharacterSetAdditions.h>
32 #import <Adium/AIChat.h>
34 #define EMOTICON_DEFAULT_PREFS                          @"EmoticonDefaults"
35 #define EMOTICONS_PATH_NAME                                     @"Emoticons"
37 //We support loading .AdiumEmoticonset, .emoticonPack, and .emoticons
38 #define ADIUM_EMOTICON_SET_PATH_EXTENSION   @"AdiumEmoticonset"
39 #define EMOTICON_PACK_PATH_EXTENSION            @"emoticonPack"
40 #define PROTEUS_EMOTICON_SET_PATH_EXTENSION @"emoticons"
42 @interface AIEmoticonController (PRIVATE)
43 - (NSDictionary *)emoticonIndex;
44 - (NSCharacterSet *)emoticonHintCharacterSet;
45 - (NSCharacterSet *)emoticonStartCharacterSet;
46 - (void)resetActiveEmoticons;
47 - (void)resetAvailableEmoticons;
48 - (NSArray *)_emoticonsPacksAvailableAtPath:(NSString *)inPath;
49 - (NSMutableAttributedString *)_convertEmoticonsInMessage:(NSAttributedString *)inMessage context:(id)context;
50 - (AIEmoticon *) _bestReplacementFromEmoticons:(NSArray *)candidateEmoticons
51                                                            withEquivalents:(NSArray *)candidateEmoticonTextEquivalents
52                                                                            context:(NSString *)serviceClassContext
53                                                                         equivalent:(NSString **)replacementString
54                                                           equivalentLength:(int *)textLength;
55 - (void)_buildCharacterSetsAndIndexEmoticons;
56 - (void)_saveActiveEmoticonPacks;
57 - (void)_saveEmoticonPackOrdering;
58 - (NSString *)_keyForPack:(AIEmoticonPack *)inPack;
59 - (void)_sortArrayOfEmoticonPacks:(NSMutableArray *)packArray;
60 @end
62 int packSortFunction(id packA, id packB, void *packOrderingArray);
64 @implementation AIEmoticonController
66 #define EMOTICONS_THEMABLE_PREFS      @"Emoticon Themable Prefs"
68 //init
69 - (id)init
71         if ((self = [super init])) {
72                 observingContent = NO;
73                 _availableEmoticonPacks = nil;
74                 _activeEmoticonPacks = nil;
75                 _activeEmoticons = nil;
76                 _emoticonHintCharacterSet = nil;
77                 _emoticonStartCharacterSet = nil;
78                 _emoticonIndexDict = nil;
79         }
80         
81         return self;
84 - (void)controllerDidLoad
86     //Create the custom emoticons directory
87     [adium createResourcePathForName:EMOTICONS_PATH_NAME];
88     
89     //Setup Preferences
90     [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:@"EmoticonDefaults" 
91                                                                                                                                                 forClass:[self class]]
92                                                                                   forGroup:PREF_GROUP_EMOTICONS];
93     
94         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_EMOTICONS];
95         
96         //Observe for installation of new emoticon sets
97         [[adium notificationCenter] addObserver:self
98                                                                    selector:@selector(xtrasChanged:)
99                                                                            name:AIXtrasDidChangeNotification
100                                                                          object:nil];
103 - (void)controllerWillClose
105         [[adium contentController] unregisterContentFilter:self];
106         [[adium preferenceController] unregisterPreferenceObserver:self];
110 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
111                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
113         //Flush our cached active emoticons
114         [self resetActiveEmoticons];
115         
116         //Enable/Disable logging
117         BOOL    emoticonsEnabled = ([[self activeEmoticons] count] != 0);
118         if (observingContent != emoticonsEnabled) {
119                 if (emoticonsEnabled) {
120                         [[adium contentController] registerContentFilter:self ofType:AIFilterDisplay direction:AIFilterIncoming];
121                         [[adium contentController] registerContentFilter:self ofType:AIFilterDisplay direction:AIFilterOutgoing];
122                         [[adium contentController] registerContentFilter:self ofType:AIFilterMessageDisplay direction:AIFilterIncoming];
123                         [[adium contentController] registerContentFilter:self ofType:AIFilterMessageDisplay direction:AIFilterOutgoing];
124                         [[adium contentController] registerContentFilter:self ofType:AIFilterTooltips direction:AIFilterIncoming];
126                 } else {
127                         [[adium contentController] unregisterContentFilter:self];
128                 }
129                 observingContent = emoticonsEnabled;
130         }
134 //Content filter -------------------------------------------------------------------------------------------------------
135 #pragma mark Content filter
136 //Filter a content object before display, inserting graphical emoticons
137 - (NSAttributedString *)filterAttributedString:(NSAttributedString *)inAttributedString context:(id)context
139     NSMutableAttributedString   *replacementMessage = nil;
140     if (inAttributedString) {
141         /* First, we do a quick scan of the message for any characters that might end up being emoticons
142          * This avoids having to do the slower, more complicated scan for the majority of messages.
143                  *
144                  * We also look for emoticons if this messsage is for a chat and it has one or more custom emoticons
145                  */
146         if (([[inAttributedString string] rangeOfCharacterFromSet:[self emoticonHintCharacterSet]].location != NSNotFound) ||
147                         ([context isKindOfClass:[AIContentObject class]] && ([[(AIContentObject *)context chat] customEmoticons]))){
148             //If an emoticon character was found, we do a more thorough scan
149             replacementMessage = [self _convertEmoticonsInMessage:inAttributedString context:context];
150         }
151     }
152     return (replacementMessage ? replacementMessage : inAttributedString);
155 //Do emoticons after the default filters
156 - (float)filterPriority
158         return LOW_FILTER_PRIORITY;
162  * @brief Perform a single emoticon replacement
164  * This method may call itself recursively to perform additional adjacent emoticon replacements
166  * @result The location in messageString of the beginning of the emoticon replaced, or NSNotFound if no replacement was made
167  */
168 - (unsigned int)replaceAnEmoticonStartingAtLocation:(unsigned *)currentLocation
169                                                                                  fromString:(NSString *)messageString
170                                                                 messageStringLength:(unsigned int)messageStringLength
171                                                    originalAttributedString:(NSAttributedString *)originalAttributedString
172                                                                                  intoString:(NSMutableAttributedString **)newMessage
173                                                                    replacementCount:(unsigned *)replacementCount
174                                                                  callingRecursively:(BOOL)callingRecursively
175                                                                 serviceClassContext:(id)serviceClassContext
176                                                   emoticonStartCharacterSet:(NSCharacterSet *)emoticonStartCharacterSet
177                                                                           emoticonIndex:(NSDictionary *)emoticonIndex
178                                                                                   isMessage:(BOOL)isMessage
180         unsigned int    originalEmoticonLocation = NSNotFound;
182         //Find the next occurence of a suspected emoticon
183         *currentLocation = [messageString rangeOfCharacterFromSet:emoticonStartCharacterSet
184                                                                                                           options:NSLiteralSearch
185                                                                                                                 range:NSMakeRange(*currentLocation, 
186                                                                                                                                                   messageStringLength - *currentLocation)].location;
187         if (*currentLocation != NSNotFound) {
188                 //Use paired arrays so multiple emoticons can qualify for the same text equivalent
189                 NSMutableArray  *candidateEmoticons = nil;
190                 NSMutableArray  *candidateEmoticonTextEquivalents = nil;                
191                 unichar         currentCharacter = [messageString characterAtIndex:*currentLocation];
192                 NSString        *currentCharacterString = [NSString stringWithFormat:@"%C", currentCharacter];
193                 NSEnumerator    *emoticonEnumerator;
194                 AIEmoticon      *emoticon;     
196                 //Check for the presence of all emoticons starting with this character
197                 emoticonEnumerator = [[emoticonIndex objectForKey:currentCharacterString] objectEnumerator];
198                 while ((emoticon = [emoticonEnumerator nextObject])) {
199                         NSEnumerator        *textEnumerator;
200                         NSString            *text;
201                         
202                         textEnumerator = [[emoticon textEquivalents] objectEnumerator];
203                         while ((text = [textEnumerator nextObject])) {
204                                 int     textLength = [text length];
205                                 
206                                 if (textLength != 0) { //Invalid emoticon files may let empty text equivalents sneak in
207                                                                            //If there is not enough room in the string for this text, we can skip it
208                                         if (*currentLocation + textLength <= messageStringLength) {
209                                                 if ([messageString compare:text
210                                                                                    options:NSLiteralSearch
211                                                                                          range:NSMakeRange(*currentLocation, textLength)] == NSOrderedSame) {
212                                                         //Ignore emoticons within links
213                                                         if ([originalAttributedString attribute:NSLinkAttributeName
214                                                                                                                         atIndex:*currentLocation
215                                                                                                          effectiveRange:nil] == nil) {
216                                                                 if (!candidateEmoticons) {
217                                                                         candidateEmoticons = [[NSMutableArray alloc] init];
218                                                                         candidateEmoticonTextEquivalents = [[NSMutableArray alloc] init];
219                                                                 }
220                                                                 
221                                                                 [candidateEmoticons addObject:emoticon];
222                                                                 [candidateEmoticonTextEquivalents addObject:text];
223                                                         }
224                                                 }
225                                         }
226                                 }
227                         }
228                 }
229                 
230                 if ([candidateEmoticons count]) {
231                         NSString                                        *replacementString;
232                         NSMutableAttributedString   *replacement;
233                         int                                                     textLength;
234                         NSRange                                         emoticonRangeInNewMessage;
235                         int                                                     amountToIncreaseCurrentLocation = 0;
237                         originalEmoticonLocation = *currentLocation;
239                         //Use the most appropriate, longest string of those which could be used for the emoticon text we found here
240                         emoticon = [self _bestReplacementFromEmoticons:candidateEmoticons
241                                                                                    withEquivalents:candidateEmoticonTextEquivalents
242                                                                                                    context:serviceClassContext
243                                                                                                 equivalent:&replacementString
244                                                                                   equivalentLength:&textLength];
245                         emoticonRangeInNewMessage = NSMakeRange(*currentLocation - *replacementCount, textLength);
247                         /* We want to show this emoticon if there is:
248                          *              It begins or ends the string
249                          *              It is bordered by spaces or line breaks or quotes on both sides
250                          *              It is bordered by a period on the left and a space or line break or quote the right
251                          *              It is bordered by emoticons on both sides or by an emoticon on the left and a period, space, or line break on the right
252                          */
253                         BOOL    acceptable = NO;
254                         if ((messageStringLength == ((originalEmoticonLocation + textLength))) || //Ends the string
255                                 (originalEmoticonLocation == 0)) { //Begins the string
256                                 acceptable = YES;
257                         }
258                         if (!acceptable) {
259                                 /* Bordered by spaces or line breaks or quotes, or by a period on the left and a space or a line break or quote on the right
260                                  * If we're being called recursively, we have a potential emoticon to our left;  we only need to check the right.
261                                  * This is also true if we're not being called recursively but there's an NSAttachmentAttribute to our left.
262                                  *              That will happen if, for example, the string is ":):) ". The first emoticon is at the start of the line and
263                                  *              so is immediately acceptable. The second should be acceptable because it is to the right of an emoticon and
264                                  *              the left of a space.
265                                  */
266                                 char    previousCharacter = [messageString characterAtIndex:(originalEmoticonLocation - 1)] ;
267                                 char    nextCharacter = [messageString characterAtIndex:(originalEmoticonLocation + textLength)] ;
269                                 if ((callingRecursively || (previousCharacter == ' ') || (previousCharacter == '\t') ||
270                                          (previousCharacter == '\n') || (previousCharacter == '\r') || (previousCharacter == '.') || (previousCharacter == '?') || (previousCharacter == '!') ||
271                                          (previousCharacter == '\"') || (previousCharacter == '\'') ||
272                                          (previousCharacter == '(') ||
273                                          (*newMessage && [*newMessage attribute:NSAttachmentAttributeName
274                                                                                                         atIndex:(emoticonRangeInNewMessage.location - 1) 
275                                                                                          effectiveRange:NULL])) &&
277                                         ((nextCharacter == ' ') || (nextCharacter == '\t') || (nextCharacter == '\n') || (nextCharacter == '\r') ||
278                                          (nextCharacter == '.') || (nextCharacter == ',') || (nextCharacter == '?') || (nextCharacter == '!') ||
279                                          (nextCharacter == ')') ||
280                                          (nextCharacter == '\"') || (nextCharacter == '\''))) {
281                                         acceptable = YES;
282                                 }
283                         }
284                         if (!acceptable) {
285                                 /* If the emoticon would end the string except for whitespace, newlines, or punctionation at the end, or it begins the string after removing
286                                  * whitespace, newlines, or punctuation at the beginning, it is acceptable even if the previous conditions weren't met.
287                                  */
288                                 static NSCharacterSet *endingTrimSet = nil;
289                                 if (!endingTrimSet) {
290                                         NSMutableCharacterSet *tempSet = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
291                                         [tempSet formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
292                                         endingTrimSet = [tempSet immutableCopy];
293                                         [tempSet release];
294                                 }
296                                 NSString        *trimmedString = [messageString stringByTrimmingCharactersInSet:endingTrimSet];
297                                 unsigned int trimmedLength = [trimmedString length];
298                                 if (trimmedLength == (originalEmoticonLocation + textLength)) {
299                                         acceptable = YES;
300                                 } else if ((originalEmoticonLocation - (messageStringLength - trimmedLength)) == 0) {
301                                         acceptable = YES;                                       
302                                 }
303                         }
304                         if (!acceptable) {
305                                 /* If we still haven't determined it to be acceptable, look ahead.
306                                  * If we do a replacement adjacent to this emoticon, we can do this one, too.
307                                  */
308                                 unsigned int newCurrentLocation = *currentLocation;
309                                 unsigned int nextEmoticonLocation;
310                                                 
311                                 /* Call ourself recursively, starting just after the end of the current emoticon candidate
312                                  * If the return value is not NSNotFound, an emoticon was found and replaced ahead of us. Discontinuous searching for the win.
313                                  */
314                                 newCurrentLocation += textLength;
315                                 nextEmoticonLocation = [self replaceAnEmoticonStartingAtLocation:&newCurrentLocation
316                                                                                                                                           fromString:messageString
317                                                                                                                          messageStringLength:messageStringLength
318                                                                                                                 originalAttributedString:originalAttributedString
319                                                                                                                                           intoString:newMessage
320                                                                                                                                 replacementCount:replacementCount
321                                                                                                                           callingRecursively:YES
322                                                                                                                          serviceClassContext:serviceClassContext
323                                                                                                            emoticonStartCharacterSet:emoticonStartCharacterSet
324                                                                                                                                    emoticonIndex:emoticonIndex
325                                                                                                                                            isMessage:isMessage];
326                                 if (nextEmoticonLocation != NSNotFound) {
327                                         if (nextEmoticonLocation == (*currentLocation + textLength)) {
328                                                 /* The next emoticon is immediately after the candidate we're looking at right now. That means
329                                                 * our current candidate is in fact an emoticon (since it borders another emoticon).
330                                                 */
331                                                 acceptable = YES;
332                                         }
333                                 }
335                                 /* Whether the current candidate is acceptable or not, we can now skip ahead to just after the next emoticon if
336                                  * there is one. If there isn't, we can skip ahead to the end of the string.
337                                  *
338                                  * We do -1 because we will do a +1 at the end of the loop no matter what.
339                                  */                             
340                                 if (newCurrentLocation != NSNotFound) {
341                                         amountToIncreaseCurrentLocation = (newCurrentLocation - *currentLocation) - 1;
342                                 } else {
343                                         amountToIncreaseCurrentLocation = (messageStringLength - *currentLocation) - 1;                                 
344                                 }
345                         }
347                         if (acceptable) {
348                                 replacement = [emoticon attributedStringWithTextEquivalent:replacementString attachImages:!isMessage];
349                                 
350                                 //grab the original attributes, to ensure that the background is not lost in a message consisting only of an emoticon
351                                 [replacement addAttributes:[originalAttributedString attributesAtIndex:originalEmoticonLocation
352                                                                                                                                                 effectiveRange:nil] 
353                                                                          range:NSMakeRange(0,1)];
354                                 
355                                 //insert the emoticon
356                                 if (!(*newMessage)) *newMessage = [originalAttributedString mutableCopy];
357                                 [*newMessage replaceCharactersInRange:emoticonRangeInNewMessage
358                                                                  withAttributedString:replacement];
359                                 
360                                 //Update where we are in the original and replacement messages
361                                 *replacementCount += textLength-1;
362                                 *currentLocation += textLength-1;
363                         } else {
364                                 //Didn't find an acceptable emoticon, so we should return NSNotFound
365                                 originalEmoticonLocation = NSNotFound;
366                         }
367                         
368                         //If appropriate, skip ahead by amountToIncreaseCurrentLocation
369                         *currentLocation += amountToIncreaseCurrentLocation;
370                 }
372                 //Always increment the loop
373                 *currentLocation += 1;
375                 [candidateEmoticons release];
376                 [candidateEmoticonTextEquivalents release];
377         }
379         return originalEmoticonLocation;
382 //Insert graphical emoticons into a string
383 - (NSMutableAttributedString *)_convertEmoticonsInMessage:(NSAttributedString *)inMessage context:(id)context
385     NSString                    *messageString = [inMessage string];
386     NSMutableAttributedString   *newMessage = nil; //We avoid creating a new string unless necessary
387         NSString                                        *serviceClassContext = nil;
388     unsigned                                    currentLocation = 0, messageStringLength;
389         NSCharacterSet                          *emoticonStartCharacterSet = [self emoticonStartCharacterSet];
390         NSDictionary                            *emoticonIndex = [self emoticonIndex];
391         //we can avoid loading images if the emoticon is headed for the wkmv, since it will just load from the original path anyway
392         BOOL                                            isMessage = NO;  
394         //Determine our service class context
395         if ([context isKindOfClass:[AIContentObject class]]) {
396                 isMessage = YES;
397                 serviceClassContext = [[[(AIContentObject *)context destination] service] serviceClass];
398                 //If there's no destination, try to use the source for context
399                 if (!serviceClassContext) {
400                         serviceClassContext = [[[(AIContentObject *)context source] service] serviceClass];
401                 }
402                 
403                 //Expand our emoticon information to include any custom emoticons in this chat
404                 NSSet *customEmoticons = [[(AIContentObject *)context chat] customEmoticons];
405                 if (customEmoticons && ![(AIContentObject *)context isOutgoing]) {
406                         /* XXX Note that we only display custom emoticons for incoming messages; we can not set our own custom emotcions
407                          * at this time
408                          */
409                         NSMutableCharacterSet   *newEmoticonStartCharacterSet = [emoticonStartCharacterSet mutableCopy];
410                         NSMutableDictionary             *newEmoticonIndex = [emoticonIndex mutableCopy];
412                         NSEnumerator *enumerator = [customEmoticons objectEnumerator];
413                         AIEmoticon       *emoticon;
414                         
415                         while ((emoticon = [enumerator nextObject])) {
416                                 NSEnumerator *textEquivalentEnumerator = [[emoticon textEquivalents] objectEnumerator];
417                                 NSString         *textEquivalent;
418                                 while ((textEquivalent = [textEquivalentEnumerator nextObject])) {
419                                         if ([textEquivalent length]) {
420                                                 NSMutableArray  *subIndex;
421                                                 NSString                *firstCharacterString;
423                                                 firstCharacterString = [NSString stringWithFormat:@"%C",[textEquivalent characterAtIndex:0]];
425                                                 //'First characters' set
426                                                 [newEmoticonStartCharacterSet addCharactersInString:firstCharacterString];
427                                                 
428                                                 // -- Index --
429                                                 //Get the index according to this emoticon's first character
430                                                 if ((subIndex = [newEmoticonIndex objectForKey:firstCharacterString])) {
431                                                         subIndex = [subIndex mutableCopy];
432                                                 } else {
433                                                         subIndex = [[NSMutableArray alloc] init];
434                                                 }
435                                                 
436                                                 [newEmoticonIndex setObject:subIndex forKey:firstCharacterString];
437                                                 [subIndex release];
438                                                 
439                                                 //Place the emoticon into that index (If it isn't already in there)
440                                                 if (![subIndex containsObject:emoticon]) {
441                                                         [subIndex addObject:emoticon];
442                                                 }
443                                         }
444                                 }
445                         }
446                         
447                         //Use our new index and character set for processing emoticons in this message
448                         emoticonIndex = [newEmoticonIndex autorelease];
449                         emoticonStartCharacterSet = [newEmoticonStartCharacterSet autorelease];
450                 }
452         } else if ([context isKindOfClass:[AIListContact class]]) {
453                 serviceClassContext = [[[[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
454                                                                                                                                                                            toContact:(AIListContact *)context] service] serviceClass];
455         } else if ([context isKindOfClass:[AIListObject class]] && [context respondsToSelector:@selector(service)]) {
456                 serviceClassContext = [[(AIListObject *)context service] serviceClass];
457         }
458         
459     //Number of characters we've replaced so far (used to calcluate placement in the destination string)
460         unsigned int    replacementCount = 0; 
462         messageStringLength = [messageString length];
463     while (currentLocation != NSNotFound && currentLocation < messageStringLength) {
464                 [self replaceAnEmoticonStartingAtLocation:&currentLocation
465                                                                            fromString:messageString
466                                                           messageStringLength:messageStringLength
467                                                  originalAttributedString:inMessage
468                                                                            intoString:&newMessage
469                                                                  replacementCount:&replacementCount
470                                                            callingRecursively:NO
471                                                           serviceClassContext:serviceClassContext
472                                                 emoticonStartCharacterSet:emoticonStartCharacterSet
473                                                                         emoticonIndex:emoticonIndex
474                                                                                 isMessage:isMessage];
475     }
477     return (newMessage ? [newMessage autorelease] : inMessage);
480 - (AIEmoticon *) _bestReplacementFromEmoticons:(NSArray *)candidateEmoticons
481                                                            withEquivalents:(NSArray *)candidateEmoticonTextEquivalents
482                                                                            context:(NSString *)serviceClassContext
483                                                                         equivalent:(NSString **)replacementString
484                                                           equivalentLength:(int *)textLength
486         unsigned        i = 0;
487         unsigned        bestIndex = 0, bestLength = 0;
488         unsigned        bestServiceAppropriateIndex = 0, bestServiceAppropriateLength = 0;
489         NSString        *serviceAppropriateReplacementString = nil;
490         unsigned        count;
491         
492         count = [candidateEmoticonTextEquivalents count];
493         while (i < count) {
494                 NSString        *thisString = [candidateEmoticonTextEquivalents objectAtIndex:i];
495                 unsigned thisLength = [thisString length];
496                 if (thisLength > bestLength) {
497                         bestLength = thisLength;
498                         bestIndex = i;
499                         *replacementString = thisString;
500                 }
502                 //If we are using service appropriate emoticons, check if this is on the right service and, if so, compare.
503                 if (thisLength > bestServiceAppropriateLength) {
504                         AIEmoticon      *thisEmoticon = [candidateEmoticons objectAtIndex:i];
505                         if ([thisEmoticon isAppropriateForServiceClass:serviceClassContext]) {
506                                 bestServiceAppropriateLength = thisLength;
507                                 bestServiceAppropriateIndex = i;
508                                 serviceAppropriateReplacementString = thisString;
509                         }
510                 }
511                 
512                 i++;
513         }
515         /* Did we get a service appropriate replacement? If so, use that rather than the current replacementString if it
516          * differs. */
517         if (serviceAppropriateReplacementString && (serviceAppropriateReplacementString != *replacementString)) {
518                 bestLength = bestServiceAppropriateLength;
519                 bestIndex = bestServiceAppropriateIndex;
520                 *replacementString = serviceAppropriateReplacementString;
521         }
523         //Return the length by reference
524         *textLength = bestLength;
526         //Return the AIEmoticon we found to be best
527     return [candidateEmoticons objectAtIndex:bestIndex];
530 //Active emoticons -----------------------------------------------------------------------------------------------------
531 #pragma mark Active emoticons
532 //Returns an array of the currently active emoticons
533 - (NSArray *)activeEmoticons
535     if (!_activeEmoticons) {
536         NSEnumerator    *enumerator;
537         AIEmoticonPack  *emoticonPack;
538         
539         //
540         _activeEmoticons = [[NSMutableArray alloc] init];
541                 
542         //Grap the emoticons from each active pack
543         enumerator = [[self activeEmoticonPacks] objectEnumerator];
544         while ((emoticonPack = [enumerator nextObject])) {
545             [_activeEmoticons addObjectsFromArray:[emoticonPack emoticons]];
546         }
547     }
548         
549     //
550     return _activeEmoticons;
553 //Returns all active emoticons, categoriezed by starting character, using a dictionary, with each value containing an array of characters
554 - (NSDictionary *)emoticonIndex
556     if (!_emoticonIndexDict) [self _buildCharacterSetsAndIndexEmoticons];
557     return _emoticonIndexDict;
561 //Disabled emoticons ---------------------------------------------------------------------------------------------------
562 #pragma mark Disabled emoticons
563 //Enabled or disable a specific emoticon
564 - (void)setEmoticon:(AIEmoticon *)inEmoticon inPack:(AIEmoticonPack *)inPack enabled:(BOOL)enabled
566     NSString                *packKey = [self _keyForPack:inPack];
567     NSMutableDictionary     *packDict = [[[adium preferenceController] preferenceForKey:packKey
568                                                                                                                                                                   group:PREF_GROUP_EMOTICONS] mutableCopy];
569     NSMutableArray          *disabledArray = [[packDict objectForKey:KEY_EMOTICON_DISABLED] mutableCopy];
570         
571     if (!packDict) packDict = [[NSMutableDictionary alloc] init];
572     if (!disabledArray) disabledArray = [[NSMutableArray alloc] init];
573     
574     //Enable/Disable the emoticon
575     if (enabled) {
576         [disabledArray removeObject:[inEmoticon name]];
577     } else {
578         [disabledArray addObject:[inEmoticon name]];
579     }
580     
581     //Update the pack (This should really be done from the prefs changed method, but it works here as well)
582     [inPack setDisabledEmoticons:disabledArray];
583     
584     //Save changes
585     [packDict setObject:disabledArray forKey:KEY_EMOTICON_DISABLED];
586         [disabledArray release];
588     [[adium preferenceController] setPreference:packDict forKey:packKey group:PREF_GROUP_EMOTICONS];
589         [packDict release];
592 //Returns the disabled emoticons in a pack
593 - (NSArray *)disabledEmoticonsInPack:(AIEmoticonPack *)inPack
595     NSDictionary    *packDict = [[adium preferenceController] preferenceForKey:[self _keyForPack:inPack]
596                                                                                                                                                  group:PREF_GROUP_EMOTICONS];
597     
598     return [packDict objectForKey:KEY_EMOTICON_DISABLED];
602 //Active emoticon packs ------------------------------------------------------------------------------------------------
603 #pragma mark Active emoticon packs
604 //Returns an array of the currently active emoticon packs
605 - (NSArray *)activeEmoticonPacks
607     if (!_activeEmoticonPacks) {
608         NSArray         *activePackNames;
609         NSEnumerator    *enumerator;
610         NSString        *packName;
611         
612         //
613         _activeEmoticonPacks = [[NSMutableArray alloc] init];
614         
615         //Get the names of our active packs
616         activePackNames = [[adium preferenceController] preferenceForKey:KEY_EMOTICON_ACTIVE_PACKS
617                                                                                                                                    group:PREF_GROUP_EMOTICONS];
618         //Use the names to build an array of the desired emoticon packs
619         enumerator = [activePackNames objectEnumerator];
620         while ((packName = [enumerator nextObject])) {
621             AIEmoticonPack  *emoticonPack = [self emoticonPackWithName:packName];
622             
623             if (emoticonPack) {
624                 [_activeEmoticonPacks addObject:emoticonPack];
625                                 [emoticonPack setIsEnabled:YES];
626             }
627         }
628                 
629                 //Sort as per the saved ordering
630                 [self _sortArrayOfEmoticonPacks:_activeEmoticonPacks];
631     }
633     return _activeEmoticonPacks;
636 - (void)setEmoticonPack:(AIEmoticonPack *)inPack enabled:(BOOL)enabled
638         if (enabled) {
639                 [_activeEmoticonPacks addObject:inPack];        
640                 [inPack setIsEnabled:YES];
641                 
642                 //Sort the active emoticon packs as per the saved ordering
643                 [self _sortArrayOfEmoticonPacks:_activeEmoticonPacks];
644         } else {
645                 [_activeEmoticonPacks removeObject:inPack];
646                 [inPack setIsEnabled:NO];
647         }
648         
649         //Save
650         [self _saveActiveEmoticonPacks];
653 //Save the active emoticon packs to preferences
654 - (void)_saveActiveEmoticonPacks
656     NSEnumerator    *enumerator;
657     AIEmoticonPack  *pack;
658     NSMutableArray  *nameArray = [NSMutableArray array];
659     
660     enumerator = [[self activeEmoticonPacks] objectEnumerator];
661     while ((pack = [enumerator nextObject])) {
662         [nameArray addObject:[pack name]];
663     }
664     
665     [[adium preferenceController] setPreference:nameArray forKey:KEY_EMOTICON_ACTIVE_PACKS group:PREF_GROUP_EMOTICONS];
669 //Available emoticon packs ---------------------------------------------------------------------------------------------
670 #pragma mark Available emoticon packs
671 //Returns an array of the available emoticon packs
672 - (NSArray *)availableEmoticonPacks
674     if (!_availableEmoticonPacks) {
675                 NSEnumerator    *enumerator;
676         NSString                *path;
677                 
678         _availableEmoticonPacks = [[NSMutableArray alloc] init];
679         
680                 //Load emoticon packs
681                 enumerator = [[adium allResourcesForName:EMOTICONS_PATH_NAME withExtensions:[NSArray arrayWithObjects:EMOTICON_PACK_PATH_EXTENSION,ADIUM_EMOTICON_SET_PATH_EXTENSION,PROTEUS_EMOTICON_SET_PATH_EXTENSION,nil]] objectEnumerator];
682                 
683                 while ((path = [enumerator nextObject])) {
684                         AIEmoticonPack  *pack = [AIEmoticonPack emoticonPackFromPath:path];
685                         
686                         if ([[pack emoticons] count]) {
687                                 [_availableEmoticonPacks addObject:pack];
688                                 [pack setDisabledEmoticons:[self disabledEmoticonsInPack:pack]];
689                         }
690                 }
691                 
692                 //Sort as per the saved ordering
693                 [self _sortArrayOfEmoticonPacks:_availableEmoticonPacks];
695                 //Build the list of active packs
696                 [self activeEmoticonPacks];
697     }
698     
699     return _availableEmoticonPacks;
702 //Returns the emoticon pack by name
703 - (AIEmoticonPack *)emoticonPackWithName:(NSString *)inName
705     NSEnumerator    *enumerator;
706     AIEmoticonPack  *emoticonPack;
707         
708     enumerator = [[self availableEmoticonPacks] objectEnumerator];
709     while ((emoticonPack = [enumerator nextObject])) {
710         if ([[emoticonPack name] isEqualToString:inName]) return emoticonPack;
711     }
712         
713     return nil;
716 - (void)xtrasChanged:(NSNotification *)notification
718         if (notification == nil || [[notification object] caseInsensitiveCompare:@"AdiumEmoticonset"] == 0) {
719                 [self resetAvailableEmoticons];
720                 [prefs emoticonXtrasDidChange];
721         }
725 //Pack ordering --------------------------------------------------------------------------------------------------------
726 #pragma mark Pack ordering
727 //Re-arrange an emoticon pack
728 - (void)moveEmoticonPacks:(NSArray *)inPacks toIndex:(int)index
729 {    
730     NSEnumerator    *enumerator;
731     AIEmoticonPack  *pack;
732     
733     //Remove each pack
734     enumerator = [inPacks objectEnumerator];
735     while ((pack = [enumerator nextObject])) {
736         if ([_availableEmoticonPacks indexOfObject:pack] < index) index--;
737         [_availableEmoticonPacks removeObject:pack];
738     }
739         
740     //Add back the packs in their new location
741     enumerator = [inPacks objectEnumerator];
742     while ((pack = [enumerator nextObject])) {
743         [_availableEmoticonPacks insertObject:pack atIndex:index];
744         index++;
745     }
746         
747     //Save our new ordering
748     [self _saveEmoticonPackOrdering];
751 - (void)_saveEmoticonPackOrdering
753     NSEnumerator                *enumerator;
754     AIEmoticonPack              *pack;
755     NSMutableArray              *nameArray = [NSMutableArray array];
756     
757     enumerator = [[self availableEmoticonPacks] objectEnumerator];
758     while ((pack = [enumerator nextObject])) {
759         [nameArray addObject:[pack name]];
760     }
761     
762         //Changing a preference will clear out our premade _activeEmoticonPacks array
763     [[adium preferenceController] setPreference:nameArray forKey:KEY_EMOTICON_PACK_ORDERING group:PREF_GROUP_EMOTICONS];        
766 - (void)_sortArrayOfEmoticonPacks:(NSMutableArray *)packArray
768         //Load the saved ordering and sort the active array based on it
769         NSArray *packOrderingArray = [[adium preferenceController] preferenceForKey:KEY_EMOTICON_PACK_ORDERING 
770                                                                                                                                                   group:PREF_GROUP_EMOTICONS];
771         //It's most likely quicker to create an empty array here than to do nil checks each time through the sort function
772         if (!packOrderingArray)
773                 packOrderingArray = [NSArray array];
774         [packArray sortUsingFunction:packSortFunction context:packOrderingArray];
777 int packSortFunction(id packA, id packB, void *packOrderingArray)
779         int packAIndex = [(NSArray *)packOrderingArray indexOfObject:[packA name]];
780         int packBIndex = [(NSArray *)packOrderingArray indexOfObject:[packB name]];
781         
782         BOOL notFoundA = (packAIndex == NSNotFound);
783         BOOL notFoundB = (packBIndex == NSNotFound);
784         
785         //Packs which aren't in the ordering index sort to the bottom
786         if (notFoundA && notFoundB) {
787                 return ([[packA name] compare:[packB name]]);
788                 
789         } else if (notFoundA) {
790                 return (NSOrderedDescending);
791                 
792         } else if (notFoundB) {
793                 return (NSOrderedAscending);
794                 
795         } else if (packAIndex > packBIndex) {
796                 return NSOrderedDescending;
797                 
798         } else {
799                 return NSOrderedAscending;
800                 
801         }
805 //Character hints for efficiency ---------------------------------------------------------------------------------------
806 #pragma mark Character hints for efficiency
807 //Returns a characterset containing characters that hint at the presence of an emoticon
808 - (NSCharacterSet *)emoticonHintCharacterSet
810     if (!_emoticonHintCharacterSet) [self _buildCharacterSetsAndIndexEmoticons];
811     return _emoticonHintCharacterSet;
814 //Returns a characterset containing all the characters that may start an emoticon
815 - (NSCharacterSet *)emoticonStartCharacterSet
817     if (!_emoticonStartCharacterSet) [self _buildCharacterSetsAndIndexEmoticons];
818     return _emoticonStartCharacterSet;
821 //For optimization, we build a list of characters that could possibly be an emoticon and will require additional scanning.
822 //We also build a dictionary categorizing the emoticons by their first character to quicken lookups.
823 - (void)_buildCharacterSetsAndIndexEmoticons
825     NSEnumerator        *emoticonEnumerator;
826     AIEmoticon          *emoticon;
827     
828     //Start with a fresh character set, and a fresh index
829         NSMutableCharacterSet   *tmpEmoticonHintCharacterSet = [[NSMutableCharacterSet alloc] init];
830         NSMutableCharacterSet   *tmpEmoticonStartCharacterSet = [[NSMutableCharacterSet alloc] init];
832         [_emoticonIndexDict release]; _emoticonIndexDict = [[NSMutableDictionary alloc] init];
833     
834     //Process all the text equivalents of each active emoticon
835     emoticonEnumerator = [[self activeEmoticons] objectEnumerator];
836     while ((emoticon = [emoticonEnumerator nextObject])) {
837         if ([emoticon isEnabled]) {
838             NSEnumerator        *textEnumerator;
839             NSString            *text;
840                         
841             textEnumerator = [[emoticon textEquivalents] objectEnumerator];
842             while ((text = [textEnumerator nextObject])) {
843                 NSMutableArray  *subIndex;
844                 unichar         firstCharacter;
845                 NSString        *firstCharacterString;
846                 
847                 if ([text length] != 0) { //Invalid emoticon files may let empty text equivalents sneak in
848                     firstCharacter = [text characterAtIndex:0];
849                     firstCharacterString = [NSString stringWithFormat:@"%C",firstCharacter];
850                     
851                     // -- Emoticon Hint Character Set --
852                     //If any letter in this text equivalent already exists in the quick scan character set, we can skip it
853                     if ([text rangeOfCharacterFromSet:tmpEmoticonHintCharacterSet].location == NSNotFound) {
854                         //Potential for optimization!: Favor punctuation characters ( :();- ) over letters (especially vowels).                
855                         [tmpEmoticonHintCharacterSet addCharactersInString:firstCharacterString];
856                     }
857                     
858                     // -- Emoticon Start Character Set --
859                     //First letter of this emoticon goes in the start set
860                     if (![tmpEmoticonStartCharacterSet characterIsMember:firstCharacter]) {
861                         [tmpEmoticonStartCharacterSet addCharactersInString:firstCharacterString];
862                     }
863                     
864                     // -- Index --
865                     //Get the index according to this emoticon's first character
866                     if (!(subIndex = [_emoticonIndexDict objectForKey:firstCharacterString])) {
867                         subIndex = [[NSMutableArray alloc] init];
868                         [_emoticonIndexDict setObject:subIndex forKey:firstCharacterString];
869                         [subIndex release];
870                     }
871                     
872                     //Place the emoticon into that index (If it isn't already in there)
873                     if (![subIndex containsObject:emoticon]) {
874                                                 //Keep emoticons in order from largest to smallest.  This prevents icons that contain other
875                                                 //icons from being masked by the smaller icons they contain.
876                                                 //This cannot work unless the emoticon equivelents are broken down.
877                                                 /*
878                                                 for (int i = 0;i < [subIndex count]; i++) {
879                                                         if ([subIndex objectAtIndex:i] equivelentLength] < ourLength]) break;
880                                                 }*/
881                         
882                                                 //Instead of adding the emoticon, add all of its equivalents... ?
883                                                 
884                                                 [subIndex addObject:emoticon];
885                     }
886                 }
887             }
888             
889         }
890     }
892         [_emoticonHintCharacterSet release]; _emoticonHintCharacterSet = [tmpEmoticonHintCharacterSet immutableCopy];
893         [tmpEmoticonHintCharacterSet release];
895     [_emoticonStartCharacterSet release]; _emoticonStartCharacterSet = [tmpEmoticonStartCharacterSet immutableCopy];
896         [tmpEmoticonStartCharacterSet release];
898         //After building all the subIndexes, sort them by length here
902 //Cache flushing -------------------------------------------------------------------------------------------------------
903 #pragma mark Cache flushing
904 //Flush any cached emoticon images (and image attachment strings)
905 - (void)flushEmoticonImageCache
907     NSEnumerator    *enumerator;
908     AIEmoticonPack  *pack;
909     
910     //Flag our emoticons as enabled/disabled
911     enumerator = [[self availableEmoticonPacks] objectEnumerator];
912     while ((pack = [enumerator nextObject])) {
913         [pack flushEmoticonImageCache];
914     }
917 //Reset the active emoticons cache
918 - (void)resetActiveEmoticons
920     [_activeEmoticonPacks release]; _activeEmoticonPacks = nil;
921     
922     [_activeEmoticons release]; _activeEmoticons = nil;
923     
924     [_emoticonHintCharacterSet release]; _emoticonHintCharacterSet = nil;
925     [_emoticonStartCharacterSet release]; _emoticonStartCharacterSet = nil;
926     [_emoticonIndexDict release]; _emoticonIndexDict = nil;
929 //Reset the available emoticons cache
930 - (void)resetAvailableEmoticons
932     [_availableEmoticonPacks release]; _availableEmoticonPacks = nil;
933     [self resetActiveEmoticons];
937 //Private --------------------------------------------------------------------------------------------------------------
938 #pragma mark Private
939 - (NSString *)_keyForPack:(AIEmoticonPack *)inPack
941         return [NSString stringWithFormat:@"Pack:%@",[inPack name]];
945 @end