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 "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;
62 int packSortFunction(id packA, id packB, void *packOrderingArray);
64 @implementation AIEmoticonController
66 #define EMOTICONS_THEMABLE_PREFS @"Emoticon Themable Prefs"
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;
84 - (void)controllerDidLoad
86 //Create the custom emoticons directory
87 [adium createResourcePathForName:EMOTICONS_PATH_NAME];
90 [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:@"EmoticonDefaults"
91 forClass:[self class]]
92 forGroup:PREF_GROUP_EMOTICONS];
94 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_EMOTICONS];
96 //Observe for installation of new emoticon sets
97 [[adium notificationCenter] addObserver:self
98 selector:@selector(xtrasChanged:)
99 name:AIXtrasDidChangeNotification
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];
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];
127 [[adium contentController] unregisterContentFilter:self];
129 observingContent = emoticonsEnabled;
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.
144 * We also look for emoticons if this messsage is for a chat and it has one or more custom emoticons
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];
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
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;
202 textEnumerator = [[emoticon textEquivalents] objectEnumerator];
203 while ((text = [textEnumerator nextObject])) {
204 int textLength = [text length];
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];
221 [candidateEmoticons addObject:emoticon];
222 [candidateEmoticonTextEquivalents addObject:text];
230 if ([candidateEmoticons count]) {
231 NSString *replacementString;
232 NSMutableAttributedString *replacement;
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
253 BOOL acceptable = NO;
254 if ((messageStringLength == ((originalEmoticonLocation + textLength))) || //Ends the string
255 (originalEmoticonLocation == 0)) { //Begins the string
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.
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 == '\''))) {
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.
288 static NSCharacterSet *endingTrimSet = nil;
289 if (!endingTrimSet) {
290 NSMutableCharacterSet *tempSet = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
291 [tempSet formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
292 endingTrimSet = [tempSet immutableCopy];
296 NSString *trimmedString = [messageString stringByTrimmingCharactersInSet:endingTrimSet];
297 unsigned int trimmedLength = [trimmedString length];
298 if (trimmedLength == (originalEmoticonLocation + textLength)) {
300 } else if ((originalEmoticonLocation - (messageStringLength - trimmedLength)) == 0) {
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.
308 unsigned int newCurrentLocation = *currentLocation;
309 unsigned int nextEmoticonLocation;
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.
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).
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.
338 * We do -1 because we will do a +1 at the end of the loop no matter what.
340 if (newCurrentLocation != NSNotFound) {
341 amountToIncreaseCurrentLocation = (newCurrentLocation - *currentLocation) - 1;
343 amountToIncreaseCurrentLocation = (messageStringLength - *currentLocation) - 1;
348 replacement = [emoticon attributedStringWithTextEquivalent:replacementString attachImages:!isMessage];
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
353 range:NSMakeRange(0,1)];
355 //insert the emoticon
356 if (!(*newMessage)) *newMessage = [originalAttributedString mutableCopy];
357 [*newMessage replaceCharactersInRange:emoticonRangeInNewMessage
358 withAttributedString:replacement];
360 //Update where we are in the original and replacement messages
361 *replacementCount += textLength-1;
362 *currentLocation += textLength-1;
364 //Didn't find an acceptable emoticon, so we should return NSNotFound
365 originalEmoticonLocation = NSNotFound;
368 //If appropriate, skip ahead by amountToIncreaseCurrentLocation
369 *currentLocation += amountToIncreaseCurrentLocation;
372 //Always increment the loop
373 *currentLocation += 1;
375 [candidateEmoticons release];
376 [candidateEmoticonTextEquivalents release];
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
394 //Determine our service class context
395 if ([context isKindOfClass:[AIContentObject class]]) {
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];
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
409 NSMutableCharacterSet *newEmoticonStartCharacterSet = [emoticonStartCharacterSet mutableCopy];
410 NSMutableDictionary *newEmoticonIndex = [emoticonIndex mutableCopy];
412 NSEnumerator *enumerator = [customEmoticons objectEnumerator];
413 AIEmoticon *emoticon;
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];
429 //Get the index according to this emoticon's first character
430 if ((subIndex = [newEmoticonIndex objectForKey:firstCharacterString])) {
431 subIndex = [subIndex mutableCopy];
433 subIndex = [[NSMutableArray alloc] init];
436 [newEmoticonIndex setObject:subIndex forKey:firstCharacterString];
439 //Place the emoticon into that index (If it isn't already in there)
440 if (![subIndex containsObject:emoticon]) {
441 [subIndex addObject:emoticon];
447 //Use our new index and character set for processing emoticons in this message
448 emoticonIndex = [newEmoticonIndex autorelease];
449 emoticonStartCharacterSet = [newEmoticonStartCharacterSet autorelease];
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];
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:¤tLocation
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];
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
487 unsigned bestIndex = 0, bestLength = 0;
488 unsigned bestServiceAppropriateIndex = 0, bestServiceAppropriateLength = 0;
489 NSString *serviceAppropriateReplacementString = nil;
492 count = [candidateEmoticonTextEquivalents count];
494 NSString *thisString = [candidateEmoticonTextEquivalents objectAtIndex:i];
495 unsigned thisLength = [thisString length];
496 if (thisLength > bestLength) {
497 bestLength = thisLength;
499 *replacementString = thisString;
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;
515 /* Did we get a service appropriate replacement? If so, use that rather than the current replacementString if it
517 if (serviceAppropriateReplacementString && (serviceAppropriateReplacementString != *replacementString)) {
518 bestLength = bestServiceAppropriateLength;
519 bestIndex = bestServiceAppropriateIndex;
520 *replacementString = serviceAppropriateReplacementString;
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;
540 _activeEmoticons = [[NSMutableArray alloc] init];
542 //Grap the emoticons from each active pack
543 enumerator = [[self activeEmoticonPacks] objectEnumerator];
544 while ((emoticonPack = [enumerator nextObject])) {
545 [_activeEmoticons addObjectsFromArray:[emoticonPack emoticons]];
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];
571 if (!packDict) packDict = [[NSMutableDictionary alloc] init];
572 if (!disabledArray) disabledArray = [[NSMutableArray alloc] init];
574 //Enable/Disable the emoticon
576 [disabledArray removeObject:[inEmoticon name]];
578 [disabledArray addObject:[inEmoticon name]];
581 //Update the pack (This should really be done from the prefs changed method, but it works here as well)
582 [inPack setDisabledEmoticons:disabledArray];
585 [packDict setObject:disabledArray forKey:KEY_EMOTICON_DISABLED];
586 [disabledArray release];
588 [[adium preferenceController] setPreference:packDict forKey:packKey group:PREF_GROUP_EMOTICONS];
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];
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;
613 _activeEmoticonPacks = [[NSMutableArray alloc] init];
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];
624 [_activeEmoticonPacks addObject:emoticonPack];
625 [emoticonPack setIsEnabled:YES];
629 //Sort as per the saved ordering
630 [self _sortArrayOfEmoticonPacks:_activeEmoticonPacks];
633 return _activeEmoticonPacks;
636 - (void)setEmoticonPack:(AIEmoticonPack *)inPack enabled:(BOOL)enabled
639 [_activeEmoticonPacks addObject:inPack];
640 [inPack setIsEnabled:YES];
642 //Sort the active emoticon packs as per the saved ordering
643 [self _sortArrayOfEmoticonPacks:_activeEmoticonPacks];
645 [_activeEmoticonPacks removeObject:inPack];
646 [inPack setIsEnabled:NO];
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];
660 enumerator = [[self activeEmoticonPacks] objectEnumerator];
661 while ((pack = [enumerator nextObject])) {
662 [nameArray addObject:[pack name]];
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;
678 _availableEmoticonPacks = [[NSMutableArray alloc] init];
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];
683 while ((path = [enumerator nextObject])) {
684 AIEmoticonPack *pack = [AIEmoticonPack emoticonPackFromPath:path];
686 if ([[pack emoticons] count]) {
687 [_availableEmoticonPacks addObject:pack];
688 [pack setDisabledEmoticons:[self disabledEmoticonsInPack:pack]];
692 //Sort as per the saved ordering
693 [self _sortArrayOfEmoticonPacks:_availableEmoticonPacks];
695 //Build the list of active packs
696 [self activeEmoticonPacks];
699 return _availableEmoticonPacks;
702 //Returns the emoticon pack by name
703 - (AIEmoticonPack *)emoticonPackWithName:(NSString *)inName
705 NSEnumerator *enumerator;
706 AIEmoticonPack *emoticonPack;
708 enumerator = [[self availableEmoticonPacks] objectEnumerator];
709 while ((emoticonPack = [enumerator nextObject])) {
710 if ([[emoticonPack name] isEqualToString:inName]) return emoticonPack;
716 - (void)xtrasChanged:(NSNotification *)notification
718 if (notification == nil || [[notification object] caseInsensitiveCompare:@"AdiumEmoticonset"] == 0) {
719 [self resetAvailableEmoticons];
720 [prefs emoticonXtrasDidChange];
725 //Pack ordering --------------------------------------------------------------------------------------------------------
726 #pragma mark Pack ordering
727 //Re-arrange an emoticon pack
728 - (void)moveEmoticonPacks:(NSArray *)inPacks toIndex:(int)index
730 NSEnumerator *enumerator;
731 AIEmoticonPack *pack;
734 enumerator = [inPacks objectEnumerator];
735 while ((pack = [enumerator nextObject])) {
736 if ([_availableEmoticonPacks indexOfObject:pack] < index) index--;
737 [_availableEmoticonPacks removeObject:pack];
740 //Add back the packs in their new location
741 enumerator = [inPacks objectEnumerator];
742 while ((pack = [enumerator nextObject])) {
743 [_availableEmoticonPacks insertObject:pack atIndex:index];
747 //Save our new ordering
748 [self _saveEmoticonPackOrdering];
751 - (void)_saveEmoticonPackOrdering
753 NSEnumerator *enumerator;
754 AIEmoticonPack *pack;
755 NSMutableArray *nameArray = [NSMutableArray array];
757 enumerator = [[self availableEmoticonPacks] objectEnumerator];
758 while ((pack = [enumerator nextObject])) {
759 [nameArray addObject:[pack name]];
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]];
782 BOOL notFoundA = (packAIndex == NSNotFound);
783 BOOL notFoundB = (packBIndex == NSNotFound);
785 //Packs which aren't in the ordering index sort to the bottom
786 if (notFoundA && notFoundB) {
787 return ([[packA name] compare:[packB name]]);
789 } else if (notFoundA) {
790 return (NSOrderedDescending);
792 } else if (notFoundB) {
793 return (NSOrderedAscending);
795 } else if (packAIndex > packBIndex) {
796 return NSOrderedDescending;
799 return NSOrderedAscending;
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;
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];
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;
841 textEnumerator = [[emoticon textEquivalents] objectEnumerator];
842 while ((text = [textEnumerator nextObject])) {
843 NSMutableArray *subIndex;
844 unichar firstCharacter;
845 NSString *firstCharacterString;
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];
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];
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];
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];
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.
878 for (int i = 0;i < [subIndex count]; i++) {
879 if ([subIndex objectAtIndex:i] equivelentLength] < ourLength]) break;
882 //Instead of adding the emoticon, add all of its equivalents... ?
884 [subIndex addObject:emoticon];
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;
910 //Flag our emoticons as enabled/disabled
911 enumerator = [[self availableEmoticonPacks] objectEnumerator];
912 while ((pack = [enumerator nextObject])) {
913 [pack flushEmoticonImageCache];
917 //Reset the active emoticons cache
918 - (void)resetActiveEmoticons
920 [_activeEmoticonPacks release]; _activeEmoticonPacks = nil;
922 [_activeEmoticons release]; _activeEmoticons = nil;
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 --------------------------------------------------------------------------------------------------------------
939 - (NSString *)_keyForPack:(AIEmoticonPack *)inPack
941 return [NSString stringWithFormat:@"Pack:%@",[inPack name]];