Unescape the HREF attribute's text before passing it to NSURL which does not expect...
[adiumx.git] / Source / ESAnnouncerPlugin.m
blobf5e270bddf881aa022accba3f7bfe68f5f028dc9
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 <Adium/AIPreferenceControllerProtocol.h>
18 #import "AISoundController.h"
19 #import "ESAnnouncerPlugin.h"
20 #import "ESAnnouncerSpeakEventAlertDetailPane.h"
21 #import "ESAnnouncerSpeakTextAlertDetailPane.h"
22 #import <Adium/AIContactAlertsControllerProtocol.h>
23 #import <AIUtilities/AIAttributedStringAdditions.h>
24 #import <AIUtilities/AIDictionaryAdditions.h>
25 #import <AIUtilities/AIDateFormatterAdditions.h>
26 #import <AIUtilities/AIImageAdditions.h>
27 #import <Adium/AIContentMessage.h>
28 #import <Adium/AIListObject.h>
30 #define CONTACT_ANNOUNCER_NIB           @"ContactAnnouncer"             //Filename of the announcer info view
31 #define ANNOUNCER_ALERT_SHORT           AILocalizedString(@"Speak Specific Text",nil)
32 #define ANNOUNCER_ALERT_LONG            AILocalizedString(@"Speak the text \"%@\"",nil)
34 #define ANNOUNCER_EVENT_ALERT_SHORT     AILocalizedString(@"Speak Event","short phrase for the contact alert which speaks the event")
35 #define ANNOUNCER_EVENT_ALERT_LONG      AILocalizedString(@"Speak the event aloud","short phrase for the contact alert which speaks the event")
37 /*!
38  * @class ESAnnouncerPlugin
39  * @brief Component which provides Speak Event and Speak Text actions
40  */
41 @implementation ESAnnouncerPlugin
43 /*!
44  * @brief Install
45  */
46 - (void)installPlugin
48     //Install our contact alerts
49         [[adium contactAlertsController] registerActionID:SPEAK_TEXT_ALERT_IDENTIFIER
50                                                                                   withHandler:self];
51         [[adium contactAlertsController] registerActionID:SPEAK_EVENT_ALERT_IDENTIFIER
52                                                                                   withHandler:self];
53     
54         [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:ANNOUNCER_DEFAULT_PREFS 
55                                                                                                                                                 forClass:[self class]] 
56                                                                                   forGroup:PREF_GROUP_ANNOUNCER];
57         
58     lastSenderString = nil;
61 /*!
62  * @brief Short description
63  * @result A short localized description of the action
64  */
65 - (NSString *)shortDescriptionForActionID:(NSString *)actionID
67         if ([actionID isEqualToString:SPEAK_TEXT_ALERT_IDENTIFIER]) {
68                 return ANNOUNCER_ALERT_SHORT;
69         } else { /*Speak Event*/
70                 return ANNOUNCER_EVENT_ALERT_SHORT;
71         }
74 /*!
75  * @brief Long description
76  * @result A longer localized description of the action which should take into account the details dictionary as appropraite.
77  */
78 - (NSString *)longDescriptionForActionID:(NSString *)actionID withDetails:(NSDictionary *)details
80         if ([actionID isEqualToString:SPEAK_TEXT_ALERT_IDENTIFIER]) {           
81                 NSString *textToSpeak = [details objectForKey:KEY_ANNOUNCER_TEXT_TO_SPEAK];
82                 
83                 if (textToSpeak && [textToSpeak length]) {
84                         return [NSString stringWithFormat:ANNOUNCER_ALERT_LONG, textToSpeak];
85                 } else {
86                         return ANNOUNCER_ALERT_SHORT;
87                 }
88         } else { /*Speak Event*/
89                 return ANNOUNCER_EVENT_ALERT_LONG;
90         }
93 /*!
94  * @brief Image
95  */
96 - (NSImage *)imageForActionID:(NSString *)actionID
98         return [NSImage imageNamed:@"AnnouncerAlert" forClass:[self class]];
102  * @brief Details pane
103  * @result An <tt>AIModularPane</tt> to use for configuring this action, or nil if no configuration is possible.
104  */
105 - (AIModularPane *)detailsPaneForActionID:(NSString *)actionID
107         if ([actionID isEqualToString:SPEAK_TEXT_ALERT_IDENTIFIER]) {
108                 return [ESAnnouncerSpeakTextAlertDetailPane actionDetailsPane];
109         } else { /*Speak Event*/
110                 return [ESAnnouncerSpeakEventAlertDetailPane actionDetailsPane];
111         }
115  * @brief Perform an action
117  * @param actionID The ID of the action to perform
118  * @param listObject The listObject associated with the event triggering the action. It may be nil
119  * @param details If set by the details pane when the action was created, the details dictionary for this particular action
120  * @param eventID The eventID which triggered this action
121  * @param userInfo Additional information associated with the event; userInfo's type will vary with the actionID.
122  */
123 - (BOOL)performActionID:(NSString *)actionID forListObject:(AIListObject *)listObject withDetails:(NSDictionary *)details triggeringEventID:(NSString *)eventID userInfo:(id)userInfo
125         NSString                        *textToSpeak = nil;
127         //Do nothing if sounds are muted for this object
128         if ([listObject soundsAreMuted]) return NO;
130         if ([actionID isEqualToString:SPEAK_TEXT_ALERT_IDENTIFIER]) {
131                 NSMutableString *userText = [[[details objectForKey:KEY_ANNOUNCER_TEXT_TO_SPEAK] mutableCopy] autorelease];
132                 
133                 if ([userText rangeOfString:@"%n"].location != NSNotFound) {
134                         NSString        *replacementText = [listObject formattedUID];
135                         
136                         [userText replaceOccurrencesOfString:@"%n"
137                                                                           withString:(replacementText ? replacementText : @"")
138                                                                                  options:NSLiteralSearch 
139                                                                                    range:NSMakeRange(0,[userText length])];
140                 }
141                 
142                 if ([userText rangeOfString:@"%a"].location != NSNotFound) {
143                         NSString        *replacementText = [listObject phoneticName];
144                         
145                         [userText replaceOccurrencesOfString:@"%a"
146                                                                           withString:(replacementText ? replacementText : @"")
147                                                                                  options:NSLiteralSearch 
148                                                                                    range:NSMakeRange(0,[userText length])];
149                         
150                 }
151                 
152                 if ([userText rangeOfString:@"%t"].location != NSNotFound) {
153                         NSString        *timeFormat = [NSDateFormatter localizedDateFormatStringShowingSeconds:YES showingAMorPM:NO];
154                         
155                         [userText replaceOccurrencesOfString:@"%t"
156                                                                           withString:[[NSDate date] descriptionWithCalendarFormat:timeFormat
157                                                                                                                                                                          timeZone:nil
158                                                                                                                                                                            locale:nil]
159                                                                                  options:NSLiteralSearch 
160                                                                                    range:NSMakeRange(0,[userText length])];
161                         
162                 }
163                 
164                 
165                 if ([userText rangeOfString:@"%m"].location != NSNotFound) {
166                         NSString                        *message;
167                         
168                         if ([[adium contactAlertsController] isMessageEvent:eventID]) {
169                                 AIContentMessage        *content = [userInfo objectForKey:@"AIContentObject"];
170                                 message = [[[content message] attributedStringByConvertingAttachmentsToStrings] string];
171                                 
172                         } else {
173                                 message = [[adium contactAlertsController] naturalLanguageDescriptionForEventID:eventID
174                                                                                                                                                                          listObject:listObject
175                                                                                                                                                                            userInfo:userInfo
176                                                                                                                                                                  includeSubject:NO];
177                         }
178                         
179                         [userText replaceOccurrencesOfString:@"%m"
180                                                                           withString:(message ? message : @"")
181                                                                                  options:NSLiteralSearch 
182                                                                                    range:NSMakeRange(0,[userText length])];                             
183                 }
184                 
185                 textToSpeak = userText;
186                 
187                 //Clear out the lastSenderString so the next Speak Event action will get tagged with the sender's name
188                 [lastSenderString release]; lastSenderString = nil;
189                 
190         } else { /*Speak Event*/        
191                 BOOL                    speakSender = [[details objectForKey:KEY_ANNOUNCER_SENDER] boolValue];
192                 BOOL                    speakTime = [[details objectForKey:KEY_ANNOUNCER_TIME] boolValue];
193                 NSString                *timeFormat;
195                 timeFormat = (speakTime ?
196                                           [NSDateFormatter localizedDateFormatStringShowingSeconds:YES showingAMorPM:NO] :
197                                           nil);
198                 
199                 //Handle messages in a custom manner
200                 if ([[adium contactAlertsController] isMessageEvent:eventID]) {
201                         AIContentMessage        *content = [userInfo objectForKey:@"AIContentObject"];
202                         NSString                        *message = [[[content message] attributedStringByConvertingAttachmentsToStrings] string];
203                         AIListObject            *source = [content source];
204                         BOOL                            isOutgoing = [content isOutgoing];
205                         BOOL                            newParagraph = NO;
206                         NSMutableString         *theMessage = [NSMutableString string];
207                         
208                         if (speakSender && !isOutgoing) {
209                                 NSString        *senderString;
210                                 
211                                 //Get the sender string
212                                 senderString = [source phoneticName];
213                                 
214                                 //Don't repeat the same sender string for messages twice in a row
215                                 if (!lastSenderString || ![senderString isEqualToString:lastSenderString]) {
216                                         NSMutableString         *senderStringToSpeak;
217                                         
218                                         //Track the sender string before modifications
219                                         [lastSenderString release]; lastSenderString = [senderString retain];
220                                         
221                                         senderStringToSpeak = [senderString mutableCopy];
222                                         
223                                         //deemphasize all words after first in sender's name, approximating human name pronunciation better
224                                         [senderStringToSpeak replaceOccurrencesOfString:@" " 
225                                                                                                                  withString:@" [[emph -]] " 
226                                                                                                                         options:NSCaseInsensitiveSearch
227                                                                                                                           range:NSMakeRange(0, [senderStringToSpeak length])];
228                                         //emphasize first word in sender's name
229                                         [theMessage appendFormat:@"[[emph +]] %@...",senderStringToSpeak];
230                                         newParagraph = YES;
231                                         
232                                         [senderStringToSpeak release];
233                                 }
234                         }
235                         
236                         //Append the date if desired, after the sender name if that was added
237                         if (timeFormat) {
238                                 [theMessage appendFormat:@" %@...",[[content date] descriptionWithCalendarFormat:timeFormat
239                                                                                                                                                                                 timeZone:nil
240                                                                                                                                                                                   locale:nil]];
241                         }
242                         
243                         if (newParagraph) [theMessage appendFormat:@" [[pmod +1; pbas +1]]"];
244                         
245                         //Finally, append the actual message
246                         [theMessage appendFormat:@" %@",message];
247                         
248                         //theMessage is now the final string which will be passed to the speech engine
249                         textToSpeak = theMessage;
250                         
251                 }else{
252                         //All non-message events use the normal naturalLanguageDescription methods, optionally prepending
253                         //the time
254                         NSString        *eventDescription;
255                         
256                         eventDescription = [[adium contactAlertsController] naturalLanguageDescriptionForEventID:eventID
257                                                                                                                                                                                   listObject:listObject
258                                                                                                                                                                                         userInfo:userInfo
259                                                                                                                                                                           includeSubject:YES];
260                         
261                         if (timeFormat) {
262                                 NSString        *timeString;
263                                 
264                                 timeString = [NSString stringWithFormat:@"%@... ",[[NSDate date] descriptionWithCalendarFormat:timeFormat
265                                                                                                                                                                                                           timeZone:nil
266                                                                                                                                                                                                                 locale:nil]];
267                                 textToSpeak = [timeString stringByAppendingString:eventDescription];
268                         } else {
269                                 textToSpeak = eventDescription;
270                         }
271                         
272                         //Clear out the lastSenderString so the next speech event will get tagged with the sender's name
273                         [lastSenderString release]; lastSenderString = nil;
274                 }
275         }
276         
277         //Do the speech, with custom voice/pitch/rate as desired
278         if (textToSpeak) {
279                 NSNumber        *pitchNumber = nil, *rateNumber = nil;
280                 NSNumber        *customPitch, *customRate;
281                 
282                 if ((customPitch = [details objectForKey:KEY_PITCH_CUSTOM]) &&
283                         ([customPitch boolValue])) {
284                         pitchNumber = [details objectForKey:KEY_PITCH];
285                 }
286                 
287                 if ((customRate = [details objectForKey:KEY_RATE_CUSTOM]) &&
288                         ([customRate boolValue])) {
289                         rateNumber = [details objectForKey:KEY_RATE];
290                 }
291                 
292                 [[adium soundController] speakText:textToSpeak
293                                                                  withVoice:[details objectForKey:KEY_VOICE_STRING]
294                                                                          pitch:(pitchNumber ? [pitchNumber floatValue] : 0.0)
295                                                                           rate:(rateNumber ? [rateNumber floatValue] : 0.0)];
296         }
297         
298         return (textToSpeak != nil);
302  * @brief Allow multiple actions?
304  * If this method returns YES, every one of this action associated with the triggering event will be executed.
305  * If this method returns NO, only the first will be.
307  * These are sound-based actions, so only allow one.
308  */
309 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID
311         return NO;
314 @end