adium/adium

Docs: Minor tweaks.
adium-1.5.11
2016-09-04, Robert Vehse
307f53385811
Docs: Minor tweaks.
/*
* Adium is the legal property of its developers, whose names are listed in the copyright file included
* with this source distribution.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation; either version 2 of the License,
* or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program; if not,
* write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#import "AIEmoticonController.h"
#import "AIEmoticon.h"
#import "AIEmoticonPack.h"
#import "AIEmoticonPreferences.h"
#import <Adium/AIContentObject.h>
#import <Adium/AIContentMessage.h>
#import <Adium/AIAccountControllerProtocol.h>
#import <Adium/AIContentControllerProtocol.h>
#import <Adium/AIAccount.h>
#import <Adium/AIListObject.h>
#import <Adium/AIListContact.h>
#import <Adium/AIService.h>
#import <AIUtilities/AIDictionaryAdditions.h>
#import <AIUtilities/AICharacterSetAdditions.h>
#import <Adium/AIChat.h>
#import <Adium/AIContentEvent.h>
#define EMOTICON_DEFAULT_PREFS @"EmoticonDefaults"
#define EMOTICONS_PATH_NAME @"Emoticons"
//We support loading .AdiumEmoticonset, .emoticonPack, and .emoticons
#define ADIUM_EMOTICON_SET_PATH_EXTENSION @"AdiumEmoticonset"
#define EMOTICON_PACK_PATH_EXTENSION @"emoticonPack"
#define PROTEUS_EMOTICON_SET_PATH_EXTENSION @"emoticons"
@interface AIEmoticonController ()
- (NSDictionary *)emoticonIndex;
- (NSCharacterSet *)emoticonHintCharacterSet;
- (NSCharacterSet *)emoticonStartCharacterSet;
- (void)resetActiveEmoticons;
- (void)resetAvailableEmoticons;
- (NSMutableAttributedString *)_convertEmoticonsInMessage:(NSAttributedString *)inMessage context:(id)context;
- (AIEmoticon *) _bestReplacementFromEmoticons:(NSArray *)candidateEmoticons
withEquivalents:(NSArray *)candidateEmoticonTextEquivalents
context:(NSString *)serviceClassContext
equivalent:(NSString **)replacementString
equivalentLength:(NSInteger *)textLength;
- (void)_buildCharacterSetsAndIndexEmoticons;
- (void)_saveActiveEmoticonPacks;
- (void)_saveEmoticonPackOrdering;
- (NSString *)_keyForPack:(AIEmoticonPack *)inPack;
- (void)_sortArrayOfEmoticonPacks:(NSMutableArray *)packArray;
@end
NSInteger packSortFunction(id packA, id packB, void *packOrderingArray);
@implementation AIEmoticonController
#define EMOTICONS_THEMABLE_PREFS @"Emoticon Themable Prefs"
//init
- (id)init
{
if ((self = [super init])) {
observingContent = NO;
_availableEmoticonPacks = nil;
_activeEmoticonPacks = nil;
_activeEmoticons = nil;
_emoticonHintCharacterSet = nil;
_emoticonStartCharacterSet = nil;
_emoticonIndexDict = nil;
}
return self;
}
- (void)controllerDidLoad
{
//Create the custom emoticons directory
[adium createResourcePathForName:EMOTICONS_PATH_NAME];
//Setup Preferences
[adium.preferenceController registerDefaults:[NSDictionary dictionaryNamed:@"EmoticonDefaults"
forClass:[self class]]
forGroup:PREF_GROUP_EMOTICONS];
[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_EMOTICONS];
//Observe for installation of new emoticon sets
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(xtrasChanged:)
name:AIXtrasDidChangeNotification
object:nil];
}
- (void)controllerWillClose
{
[adium.contentController unregisterContentFilter:self];
[adium.preferenceController unregisterPreferenceObserver:self];
}
- (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
{
//Flush our cached active emoticons
[self resetActiveEmoticons];
//Enable/Disable logging
BOOL emoticonsEnabled = ([[self activeEmoticons] count] != 0);
if (observingContent != emoticonsEnabled) {
if (emoticonsEnabled) {
[adium.contentController registerContentFilter:self ofType:AIFilterDisplay direction:AIFilterIncoming];
[adium.contentController registerContentFilter:self ofType:AIFilterDisplay direction:AIFilterOutgoing];
[adium.contentController registerContentFilter:self ofType:AIFilterMessageDisplay direction:AIFilterIncoming];
[adium.contentController registerContentFilter:self ofType:AIFilterMessageDisplay direction:AIFilterOutgoing];
[adium.contentController registerContentFilter:self ofType:AIFilterTooltips direction:AIFilterIncoming];
} else {
[adium.contentController unregisterContentFilter:self];
}
observingContent = emoticonsEnabled;
}
}
//Content filter -------------------------------------------------------------------------------------------------------
#pragma mark Content filter
//Filter a content object before display, inserting graphical emoticons
- (NSAttributedString *)filterAttributedString:(NSAttributedString *)inAttributedString context:(id)context
{
NSMutableAttributedString *replacementMessage = nil;
// We want to filter some status event messages (e.g. changes in status messages), but not fileTransfer messages.
// Filenames, afterall, should not have emoticons in them.
if (inAttributedString &&
!([context isKindOfClass:[AIContentEvent class]] &&
[[(AIContentEvent *)context type] rangeOfString:@"fileTransfer"].location == NSNotFound)) {
/* First, we do a quick scan of the message for any characters that might end up being emoticons
* This avoids having to do the slower, more complicated scan for the majority of messages.
*
* We also look for emoticons if this messsage is for a chat and it has one or more custom emoticons
*/
if (([[inAttributedString string] rangeOfCharacterFromSet:[self emoticonHintCharacterSet]].location != NSNotFound) ||
([context isKindOfClass:[AIContentObject class]] && ([[(AIContentObject *)context chat] customEmoticons]))){
//If an emoticon character was found, we do a more thorough scan
replacementMessage = [self _convertEmoticonsInMessage:inAttributedString context:context];
}
}
return (replacementMessage ? replacementMessage : inAttributedString);
}
//Do emoticons after the default filters
- (CGFloat)filterPriority
{
return LOW_FILTER_PRIORITY;
}
/*!
* @brief Perform a single emoticon replacement
*
* This method may call itself recursively to perform additional adjacent emoticon replacements
*
* @result The location in messageString of the beginning of the emoticon replaced, or NSNotFound if no replacement was made
*/
- (NSUInteger)replaceAnEmoticonStartingAtLocation:(NSUInteger *)currentLocation
fromString:(NSString *)messageString
messageStringLength:(NSUInteger)messageStringLength
originalAttributedString:(NSAttributedString *)originalAttributedString
intoString:(NSMutableAttributedString **)newMessage
replacementCount:(NSUInteger *)replacementCount
callingRecursively:(BOOL)callingRecursively
serviceClassContext:(id)serviceClassContext
emoticonStartCharacterSet:(NSCharacterSet *)emoticonStartCharacterSet
emoticonIndex:(NSDictionary *)emoticonIndex
isMessage:(BOOL)isMessage
{
NSUInteger originalEmoticonLocation = NSNotFound;
//Find the next occurence of a suspected emoticon
*currentLocation = [messageString rangeOfCharacterFromSet:emoticonStartCharacterSet
options:NSLiteralSearch
range:NSMakeRange(*currentLocation,
messageStringLength - *currentLocation)].location;
if (*currentLocation != NSNotFound) {
//Use paired arrays so multiple emoticons can qualify for the same text equivalent
NSMutableArray *candidateEmoticons = nil;
NSMutableArray *candidateEmoticonTextEquivalents = nil;
unichar currentCharacter = [messageString characterAtIndex:*currentLocation];
NSString *currentCharacterString = [NSString stringWithFormat:@"%C", currentCharacter];
//Check for the presence of all emoticons starting with this character
for (AIEmoticon *emoticon in [emoticonIndex objectForKey:currentCharacterString]) {
for (NSString *text in [emoticon textEquivalents]) {
NSInteger textLength = [text length];
if (textLength != 0) { //Invalid emoticon files may let empty text equivalents sneak in
//If there is not enough room in the string for this text, we can skip it
if (*currentLocation + textLength <= messageStringLength) {
if ([messageString compare:text
options:NSLiteralSearch
range:NSMakeRange(*currentLocation, textLength)] == NSOrderedSame) {
//Ignore emoticons within links
if ([originalAttributedString attribute:NSLinkAttributeName
atIndex:*currentLocation
effectiveRange:nil] == nil) {
if (!candidateEmoticons) {
candidateEmoticons = [[NSMutableArray alloc] init];
candidateEmoticonTextEquivalents = [[NSMutableArray alloc] init];
}
[candidateEmoticons addObject:emoticon];
[candidateEmoticonTextEquivalents addObject:text];
}
}
}
}
}
}
BOOL currentLocationNeedsUpdate = YES;
if ([candidateEmoticons count]) {
NSString *replacementString;
NSMutableAttributedString *replacement;
NSInteger textLength;
NSRange emoticonRangeInNewMessage;
originalEmoticonLocation = *currentLocation;
//Use the most appropriate, longest string of those which could be used for the emoticon text we found here
AIEmoticon *emoticon = [self _bestReplacementFromEmoticons:candidateEmoticons
withEquivalents:candidateEmoticonTextEquivalents
context:serviceClassContext
equivalent:&replacementString
equivalentLength:&textLength];
emoticonRangeInNewMessage = NSMakeRange(*currentLocation - *replacementCount, textLength);
/* We want to show this emoticon if there is:
* It begins or ends the string
* It is bordered by spaces or line breaks or quotes on both sides
* It is bordered by a period on the left and a space or line break or quote the right
* 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
*/
BOOL acceptable = NO;
if ((messageStringLength == ((originalEmoticonLocation + textLength))) || //Ends the string
(originalEmoticonLocation == 0)) { //Begins the string
acceptable = YES;
}
if (!acceptable) {
/* 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
* If we're being called recursively, we have a potential emoticon to our left; we only need to check the right.
* This is also true if we're not being called recursively but there's an NSAttachmentAttribute to our left.
* That will happen if, for example, the string is ":):) ". The first emoticon is at the start of the line and
* so is immediately acceptable. The second should be acceptable because it is to the right of an emoticon and
* the left of a space.
*/
char previousCharacter = [messageString characterAtIndex:(originalEmoticonLocation - 1)] ;
char nextCharacter = [messageString characterAtIndex:(originalEmoticonLocation + textLength)] ;
if ((callingRecursively || (previousCharacter == ' ') || (previousCharacter == '\t') ||
(previousCharacter == '\n') || (previousCharacter == '\r') || (previousCharacter == '.') || (previousCharacter == '?') || (previousCharacter == '!') ||
(previousCharacter == '\"') || (previousCharacter == '\'') ||
(previousCharacter == '(') || (previousCharacter == '*') ||
(*newMessage && [*newMessage attribute:NSAttachmentAttributeName
atIndex:(emoticonRangeInNewMessage.location - 1)
effectiveRange:NULL])) &&
((nextCharacter == ' ') || (nextCharacter == '\t') || (nextCharacter == '\n') || (nextCharacter == '\r') ||
(nextCharacter == '.') || (nextCharacter == ',') || (nextCharacter == '?') || (nextCharacter == '!') ||
(nextCharacter == ')') || (nextCharacter == '*') ||
(nextCharacter == '\"') || (nextCharacter == '\''))) {
acceptable = YES;
}
}
if (!acceptable) {
/* If the emoticon would end the string except for whitespace, newlines, or punctionation at the end, or it begins the string after removing
* whitespace, newlines, or punctuation at the beginning, it is acceptable even if the previous conditions weren't met.
*/
NSCharacterSet *endingTrimSet = nil;
static NSMutableDictionary *endingSetDict = nil;
if(!endingSetDict) {
endingSetDict = [[NSMutableDictionary alloc] initWithCapacity:10];
}
if (!(endingTrimSet = [endingSetDict objectForKey:replacementString])) {
NSMutableCharacterSet *tempSet = [[NSCharacterSet punctuationCharacterSet] mutableCopy];
[tempSet formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
[tempSet formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
//remove any characters *in* the replacement string from the trimming set
[tempSet removeCharactersInString:replacementString];
[endingSetDict setObject:[[tempSet immutableCopy] autorelease] forKey:replacementString];
[tempSet release];
endingTrimSet = [endingSetDict objectForKey:replacementString];
}
NSString *trimmedString = [messageString stringByTrimmingCharactersInSet:endingTrimSet];
NSUInteger trimmedLength = [trimmedString length];
if (trimmedLength == (originalEmoticonLocation + textLength)) {
// Replace at end of string
acceptable = YES;
} else if ([trimmedString characterAtIndex:0] == [replacementString characterAtIndex:0]) {
// Replace at start of string
acceptable = YES;
}
}
if (!acceptable) {
/* If we still haven't determined it to be acceptable, look ahead.
* If we do a replacement adjacent to this emoticon, we can do this one, too.
*/
NSUInteger newCurrentLocation = *currentLocation;
NSUInteger nextEmoticonLocation;
/* Call ourself recursively, starting just after the end of the current emoticon candidate
* If the return value is not NSNotFound, an emoticon was found and replaced ahead of us. Discontinuous searching for the win.
*/
newCurrentLocation += textLength;
nextEmoticonLocation = [self replaceAnEmoticonStartingAtLocation:&newCurrentLocation
fromString:messageString
messageStringLength:messageStringLength
originalAttributedString:originalAttributedString
intoString:newMessage
replacementCount:replacementCount
callingRecursively:YES
serviceClassContext:serviceClassContext
emoticonStartCharacterSet:emoticonStartCharacterSet
emoticonIndex:emoticonIndex
isMessage:isMessage];
if (nextEmoticonLocation != NSNotFound) {
if (nextEmoticonLocation == (*currentLocation + textLength)) {
/* The next emoticon is immediately after the candidate we're looking at right now. That means
* our current candidate is in fact an emoticon (since it borders another emoticon).
*/
acceptable = YES;
}
currentLocationNeedsUpdate = NO;
*currentLocation = newCurrentLocation;
} else {
/* If there isn't a next emoticon, we can skip ahead to the end of the string. */
*currentLocation = messageStringLength;
currentLocationNeedsUpdate = NO;
}
}
if (acceptable) {
replacement = [emoticon attributedStringWithTextEquivalent:replacementString attachImages:!isMessage];
NSDictionary *originalAttributes = [originalAttributedString attributesAtIndex:originalEmoticonLocation
effectiveRange:nil];
originalAttributes = [originalAttributes dictionaryWithDifferenceWithSetOfKeys:[NSSet setWithObject:NSAttachmentAttributeName]];
//grab the original attributes, to ensure that the background is not lost in a message consisting only of an emoticon
[replacement addAttributes:originalAttributes
range:NSMakeRange(0,1)];
//insert the emoticon
if (!(*newMessage)) *newMessage = [originalAttributedString mutableCopy];
[*newMessage replaceCharactersInRange:emoticonRangeInNewMessage
withAttributedString:replacement];
//Update where we are in the original and replacement messages
*replacementCount += textLength-1;
if (currentLocationNeedsUpdate)
*currentLocation += textLength-1;
} else {
//Didn't find an acceptable emoticon, so we should return NSNotFound
originalEmoticonLocation = NSNotFound;
}
}
//Always increment the loop
if (currentLocationNeedsUpdate) {
*currentLocation += 1;
}
[candidateEmoticons release];
[candidateEmoticonTextEquivalents release];
}
return originalEmoticonLocation;
}
//Insert graphical emoticons into a string
- (NSAttributedString *)_convertEmoticonsInMessage:(NSAttributedString *)inMessage context:(id)context
{
NSString *messageString = [inMessage string];
NSMutableAttributedString *newMessage = nil; //We avoid creating a new string unless necessary
NSString *serviceClassContext = nil;
NSUInteger currentLocation = 0, messageStringLength;
NSCharacterSet *emoticonStartCharacterSet = self.emoticonStartCharacterSet;
NSDictionary *emoticonIndex = self.emoticonIndex;
//we can avoid loading images if the emoticon is headed for the wkmv, since it will just load from the original path anyway
BOOL isMessage = NO;
//Determine our service class context
if ([context isKindOfClass:[AIContentObject class]]) {
isMessage = YES;
serviceClassContext = ((AIContentObject *)context).destination.service.serviceClass;
//If there's no destination, try to use the source for context
if (!serviceClassContext) {
serviceClassContext = ((AIContentObject *)context).source.service.serviceClass;
}
//Expand our emoticon information to include any custom emoticons in this chat
NSSet *customEmoticons = ((AIContentObject *)context).chat.customEmoticons;
if (customEmoticons && !((AIContentObject *)context).isOutgoing) {
/* XXX Note that we only display custom emoticons for incoming messages; we can not set our own custom emotcions
* at this time
*/
NSMutableCharacterSet *newEmoticonStartCharacterSet = [emoticonStartCharacterSet mutableCopy];
NSMutableDictionary *newEmoticonIndex = [emoticonIndex mutableCopy];
AIEmoticon *emoticon;
for (emoticon in customEmoticons) {
for (NSString *textEquivalent in emoticon.textEquivalents) {
if (textEquivalent.length) {
NSMutableArray *subIndex;
NSString *firstCharacterString;
firstCharacterString = [NSString stringWithFormat:@"%C",[textEquivalent characterAtIndex:0]];
//'First characters' set
[newEmoticonStartCharacterSet addCharactersInString:firstCharacterString];
// -- Index --
//Get the index according to this emoticon's first character
if ((subIndex = [newEmoticonIndex objectForKey:firstCharacterString])) {
subIndex = [subIndex mutableCopy];
} else {
subIndex = [[NSMutableArray alloc] init];
}
[newEmoticonIndex setObject:subIndex forKey:firstCharacterString];
[subIndex release];
//Place the emoticon into that index (If it isn't already in there)
if (![subIndex containsObject:emoticon]) {
[subIndex addObject:emoticon];
}
}
}
}
//Use our new index and character set for processing emoticons in this message
emoticonIndex = [newEmoticonIndex autorelease];
emoticonStartCharacterSet = [newEmoticonStartCharacterSet autorelease];
}
} else if ([context isKindOfClass:[AIListContact class]]) {
serviceClassContext = [[[adium.accountController preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
toContact:(AIListContact *)context] service] serviceClass];
} else if ([context isKindOfClass:[AIListObject class]] && [context respondsToSelector:@selector(service)]) {
serviceClassContext = ((AIListObject *)context).service.serviceClass;
}
//Number of characters we've replaced so far (used to calcluate placement in the destination string)
NSUInteger replacementCount = 0;
messageStringLength = [messageString length];
while (currentLocation != NSNotFound && currentLocation < messageStringLength) {
[self replaceAnEmoticonStartingAtLocation:&currentLocation
fromString:messageString
messageStringLength:messageStringLength
originalAttributedString:inMessage
intoString:&newMessage
replacementCount:&replacementCount
callingRecursively:NO
serviceClassContext:serviceClassContext
emoticonStartCharacterSet:emoticonStartCharacterSet
emoticonIndex:emoticonIndex
isMessage:isMessage];
}
return (newMessage ? [newMessage autorelease] : inMessage);
}
- (AIEmoticon *) _bestReplacementFromEmoticons:(NSArray *)candidateEmoticons
withEquivalents:(NSArray *)candidateEmoticonTextEquivalents
context:(NSString *)serviceClassContext
equivalent:(NSString **)replacementString
equivalentLength:(NSInteger *)textLength
{
NSUInteger i = 0;
NSUInteger bestIndex = 0, bestLength = 0;
NSUInteger bestServiceAppropriateIndex = 0, bestServiceAppropriateLength = 0;
NSString *serviceAppropriateReplacementString = nil;
NSUInteger count;
count = [candidateEmoticonTextEquivalents count];
while (i < count) {
NSString *thisString = [candidateEmoticonTextEquivalents objectAtIndex:i];
NSUInteger thisLength = [thisString length];
if (thisLength > bestLength) {
bestLength = thisLength;
bestIndex = i;
*replacementString = thisString;
}
//If we are using service appropriate emoticons, check if this is on the right service and, if so, compare.
if (thisLength > bestServiceAppropriateLength) {
AIEmoticon *thisEmoticon = [candidateEmoticons objectAtIndex:i];
if ([thisEmoticon isAppropriateForServiceClass:serviceClassContext]) {
bestServiceAppropriateLength = thisLength;
bestServiceAppropriateIndex = i;
serviceAppropriateReplacementString = thisString;
}
}
i++;
}
/* Did we get a service appropriate replacement? If so, use that rather than the current replacementString if it
* differs. */
if (serviceAppropriateReplacementString && (serviceAppropriateReplacementString != *replacementString)) {
bestLength = bestServiceAppropriateLength;
bestIndex = bestServiceAppropriateIndex;
*replacementString = serviceAppropriateReplacementString;
}
//Return the length by reference
*textLength = bestLength;
//Return the AIEmoticon we found to be best
return [candidateEmoticons objectAtIndex:bestIndex];
}
//Active emoticons -----------------------------------------------------------------------------------------------------
#pragma mark Active emoticons
//Returns an array of the currently active emoticons
- (NSArray *)activeEmoticons
{
if (!_activeEmoticons) {
_activeEmoticons = [[NSMutableArray alloc] init];
//Grap the emoticons from each active pack
for (AIEmoticonPack *emoticonPack in [self activeEmoticonPacks]) {
[_activeEmoticons addObjectsFromArray:[emoticonPack emoticons]];
}
}
//
return _activeEmoticons;
}
//Returns all active emoticons, categoriezed by starting character, using a dictionary, with each value containing an array of characters
- (NSDictionary *)emoticonIndex
{
if (!_emoticonIndexDict) [self _buildCharacterSetsAndIndexEmoticons];
return _emoticonIndexDict;
}
//Disabled emoticons ---------------------------------------------------------------------------------------------------
#pragma mark Disabled emoticons
//Enabled or disable a specific emoticon
- (void)setEmoticon:(AIEmoticon *)inEmoticon inPack:(AIEmoticonPack *)inPack enabled:(BOOL)enabled
{
NSString *packKey = [self _keyForPack:inPack];
NSMutableDictionary *packDict = [[adium.preferenceController preferenceForKey:packKey
group:PREF_GROUP_EMOTICONS] mutableCopy];
NSMutableArray *disabledArray = [[packDict objectForKey:KEY_EMOTICON_DISABLED] mutableCopy];
if (!packDict) packDict = [[NSMutableDictionary alloc] init];
if (!disabledArray) disabledArray = [[NSMutableArray alloc] init];
//Enable/Disable the emoticon
if (enabled) {
[disabledArray removeObject:[inEmoticon name]];
} else {
[disabledArray addObject:[inEmoticon name]];
}
//Update the pack (This should really be done from the prefs changed method, but it works here as well)
[inPack setDisabledEmoticons:disabledArray];
//Save changes
[packDict setObject:disabledArray forKey:KEY_EMOTICON_DISABLED];
[disabledArray release];
[adium.preferenceController setPreference:packDict forKey:packKey group:PREF_GROUP_EMOTICONS];
[packDict release];
}
//Returns the disabled emoticons in a pack
- (NSArray *)disabledEmoticonsInPack:(AIEmoticonPack *)inPack
{
NSDictionary *packDict = [adium.preferenceController preferenceForKey:[self _keyForPack:inPack]
group:PREF_GROUP_EMOTICONS];
return [packDict objectForKey:KEY_EMOTICON_DISABLED];
}
//Active emoticon packs ------------------------------------------------------------------------------------------------
#pragma mark Active emoticon packs
//Returns an array of the currently active emoticon packs
- (NSArray *)activeEmoticonPacks
{
if (!_activeEmoticonPacks) {
NSArray *activePackNames;
NSString *packName;
//
_activeEmoticonPacks = [[NSMutableArray alloc] init];
//Get the names of our active packs
activePackNames = [adium.preferenceController preferenceForKey:KEY_EMOTICON_ACTIVE_PACKS
group:PREF_GROUP_EMOTICONS];
//Use the names to build an array of the desired emoticon packs
for (packName in activePackNames) {
AIEmoticonPack *emoticonPack = [self emoticonPackWithName:packName];
if (emoticonPack) {
[_activeEmoticonPacks addObject:emoticonPack];
[emoticonPack setIsEnabled:YES];
}
}
//Sort as per the saved ordering
[self _sortArrayOfEmoticonPacks:_activeEmoticonPacks];
}
return _activeEmoticonPacks;
}
- (void)setEmoticonPack:(AIEmoticonPack *)inPack enabled:(BOOL)enabled
{
if (enabled) {
[_activeEmoticonPacks addObject:inPack];
[inPack setIsEnabled:YES];
//Sort the active emoticon packs as per the saved ordering
[self _sortArrayOfEmoticonPacks:_activeEmoticonPacks];
} else {
[_activeEmoticonPacks removeObject:inPack];
[inPack setIsEnabled:NO];
}
//Save
[self _saveActiveEmoticonPacks];
}
//Save the active emoticon packs to preferences
- (void)_saveActiveEmoticonPacks
{
NSMutableArray *nameArray = [NSMutableArray array];
for (AIEmoticonPack *emoticonPack in [self activeEmoticonPacks]) {
[nameArray addObject:emoticonPack.name];
}
[adium.preferenceController setPreference:nameArray forKey:KEY_EMOTICON_ACTIVE_PACKS group:PREF_GROUP_EMOTICONS];
}
//Available emoticon packs ---------------------------------------------------------------------------------------------
#pragma mark Available emoticon packs
//Returns an array of the available emoticon packs
- (NSArray *)availableEmoticonPacks
{
if (!_availableEmoticonPacks) {
_availableEmoticonPacks = [[NSMutableArray alloc] init];
//Load emoticon packs
for (NSString *path in [adium allResourcesForName:EMOTICONS_PATH_NAME
withExtensions:[NSArray arrayWithObjects:
EMOTICON_PACK_PATH_EXTENSION,
ADIUM_EMOTICON_SET_PATH_EXTENSION,
PROTEUS_EMOTICON_SET_PATH_EXTENSION,
nil]]) {
AIEmoticonPack *pack = [AIEmoticonPack emoticonPackFromPath:path];
if (pack.emoticons.count) {
[_availableEmoticonPacks addObject:pack];
[pack setDisabledEmoticons:[self disabledEmoticonsInPack:pack]];
}
}
//Sort as per the saved ordering
[self _sortArrayOfEmoticonPacks:_availableEmoticonPacks];
//Build the list of active packs
[self activeEmoticonPacks];
}
return _availableEmoticonPacks;
}
//Returns the emoticon pack by name
- (AIEmoticonPack *)emoticonPackWithName:(NSString *)inName
{
for (AIEmoticonPack *emoticonPack in self.availableEmoticonPacks) {
if ([emoticonPack.name isEqualToString:inName]) return emoticonPack;
}
return nil;
}
- (void)xtrasChanged:(NSNotification *)notification
{
if (notification == nil || [[notification object] caseInsensitiveCompare:@"AdiumEmoticonset"] == NSOrderedSame) {
[self resetAvailableEmoticons];
[prefs emoticonXtrasDidChange];
}
}
//Pack ordering --------------------------------------------------------------------------------------------------------
#pragma mark Pack ordering
//Re-arrange an emoticon pack
- (void)moveEmoticonPacks:(NSArray *)inPacks toIndex:(NSUInteger)idx
{
//Remove each pack
for (AIEmoticonPack *pack in inPacks) {
if ([_availableEmoticonPacks indexOfObject:pack] < idx) idx--;
[_availableEmoticonPacks removeObject:pack];
}
//Add back the packs in their new location
for (AIEmoticonPack *pack in inPacks) {
[_availableEmoticonPacks insertObject:pack atIndex:idx];
idx++;
}
//Save our new ordering
[self _saveEmoticonPackOrdering];
}
- (void)_saveEmoticonPackOrdering
{
NSMutableArray *nameArray = [NSMutableArray array];
for (AIEmoticonPack *pack in self.availableEmoticonPacks) {
[nameArray addObject:pack.name];
}
//Changing a preference will clear out our premade _activeEmoticonPacks array
[adium.preferenceController setPreference:nameArray forKey:KEY_EMOTICON_PACK_ORDERING group:PREF_GROUP_EMOTICONS];
}
- (void)_sortArrayOfEmoticonPacks:(NSMutableArray *)packArray
{
//Load the saved ordering and sort the active array based on it
NSArray *packOrderingArray = [adium.preferenceController preferenceForKey:KEY_EMOTICON_PACK_ORDERING
group:PREF_GROUP_EMOTICONS];
//It's most likely quicker to create an empty array here than to do nil checks each time through the sort function
if (!packOrderingArray)
packOrderingArray = [NSArray array];
[packArray sortUsingFunction:packSortFunction context:packOrderingArray];
}
NSInteger packSortFunction(id packA, id packB, void *packOrderingArray)
{
NSInteger packAIndex = [(NSArray *)packOrderingArray indexOfObject:[packA name]];
NSInteger packBIndex = [(NSArray *)packOrderingArray indexOfObject:[packB name]];
BOOL notFoundA = (packAIndex == NSNotFound);
BOOL notFoundB = (packBIndex == NSNotFound);
//Packs which aren't in the ordering index sort to the bottom
if (notFoundA && notFoundB) {
return ([[packA name] compare:[packB name]]);
} else if (notFoundA) {
return (NSOrderedDescending);
} else if (notFoundB) {
return (NSOrderedAscending);
} else if (packAIndex > packBIndex) {
return NSOrderedDescending;
} else {
return NSOrderedAscending;
}
}
//Character hints for efficiency ---------------------------------------------------------------------------------------
#pragma mark Character hints for efficiency
//Returns a characterset containing characters that hint at the presence of an emoticon
- (NSCharacterSet *)emoticonHintCharacterSet
{
if (!_emoticonHintCharacterSet) [self _buildCharacterSetsAndIndexEmoticons];
return _emoticonHintCharacterSet;
}
//Returns a characterset containing all the characters that may start an emoticon
- (NSCharacterSet *)emoticonStartCharacterSet
{
if (!_emoticonStartCharacterSet) [self _buildCharacterSetsAndIndexEmoticons];
return _emoticonStartCharacterSet;
}
//For optimization, we build a list of characters that could possibly be an emoticon and will require additional scanning.
//We also build a dictionary categorizing the emoticons by their first character to quicken lookups.
- (void)_buildCharacterSetsAndIndexEmoticons
{
//Start with a fresh character set, and a fresh index
NSMutableCharacterSet *tmpEmoticonHintCharacterSet = [[NSMutableCharacterSet alloc] init];
NSMutableCharacterSet *tmpEmoticonStartCharacterSet = [[NSMutableCharacterSet alloc] init];
[_emoticonIndexDict release]; _emoticonIndexDict = [[NSMutableDictionary alloc] init];
//Process all the text equivalents of each active emoticon
for (AIEmoticon *emoticon in self.activeEmoticons) {
if (emoticon.isEnabled) {
for (NSString *text in emoticon.textEquivalents) {
NSMutableArray *subIndex;
unichar firstCharacter;
NSString *firstCharacterString;
if ([text length] != 0) { //Invalid emoticon files may let empty text equivalents sneak in
firstCharacter = [text characterAtIndex:0];
firstCharacterString = [NSString stringWithFormat:@"%C",firstCharacter];
// -- Emoticon Hint Character Set --
//If any letter in this text equivalent already exists in the quick scan character set, we can skip it
if ([text rangeOfCharacterFromSet:tmpEmoticonHintCharacterSet].location == NSNotFound) {
//Potential for optimization!: Favor punctuation characters ( :();- ) over letters (especially vowels).
[tmpEmoticonHintCharacterSet addCharactersInString:firstCharacterString];
}
// -- Emoticon Start Character Set --
//First letter of this emoticon goes in the start set
if (![tmpEmoticonStartCharacterSet characterIsMember:firstCharacter]) {
[tmpEmoticonStartCharacterSet addCharactersInString:firstCharacterString];
}
// -- Index --
//Get the index according to this emoticon's first character
if (!(subIndex = [_emoticonIndexDict objectForKey:firstCharacterString])) {
subIndex = [[NSMutableArray alloc] init];
[_emoticonIndexDict setObject:subIndex forKey:firstCharacterString];
[subIndex release];
}
//Place the emoticon into that index (If it isn't already in there)
if (![subIndex containsObject:emoticon]) {
//Keep emoticons in order from largest to smallest. This prevents icons that contain other
//icons from being masked by the smaller icons they contain.
//This cannot work unless the emoticon equivelents are broken down.
/*
for (int i = 0;i < [subIndex count]; i++) {
if ([subIndex objectAtIndex:i] equivelentLength] < ourLength]) break;
}*/
//Instead of adding the emoticon, add all of its equivalents... ?
[subIndex addObject:emoticon];
}
}
}
}
}
[_emoticonHintCharacterSet release]; _emoticonHintCharacterSet = [tmpEmoticonHintCharacterSet immutableCopy];
[tmpEmoticonHintCharacterSet release];
[_emoticonStartCharacterSet release]; _emoticonStartCharacterSet = [tmpEmoticonStartCharacterSet immutableCopy];
[tmpEmoticonStartCharacterSet release];
//After building all the subIndexes, sort them by length here
}
//Cache flushing -------------------------------------------------------------------------------------------------------
#pragma mark Cache flushing
//Flush any cached emoticon images (and image attachment strings)
- (void)flushEmoticonImageCache
{
for (AIEmoticonPack *pack in self.availableEmoticonPacks) {
[pack flushEmoticonImageCache];
}
}
//Reset the active emoticons cache
- (void)resetActiveEmoticons
{
[_activeEmoticonPacks release]; _activeEmoticonPacks = nil;
[_activeEmoticons release]; _activeEmoticons = nil;
[_emoticonHintCharacterSet release]; _emoticonHintCharacterSet = nil;
[_emoticonStartCharacterSet release]; _emoticonStartCharacterSet = nil;
[_emoticonIndexDict release]; _emoticonIndexDict = nil;
}
//Reset the available emoticons cache
- (void)resetAvailableEmoticons
{
[_availableEmoticonPacks release]; _availableEmoticonPacks = nil;
[self resetActiveEmoticons];
}
//Private --------------------------------------------------------------------------------------------------------------
#pragma mark Private
- (NSString *)_keyForPack:(AIEmoticonPack *)inPack
{
return [NSString stringWithFormat:@"Pack:%@",[inPack name]];
}
@end