Not all subtitle tracks are SDH
[WebKit-https.git] / Source / WebCore / page / CaptionUserPreferencesMac.mm
1 /*
2  * Copyright (C) 2012, 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #import "config.h"
27
28 #if ENABLE(VIDEO_TRACK)
29
30 #import "CaptionUserPreferencesMac.h"
31
32 #import "ColorMac.h"
33 #import "CoreText/CoreText.h"
34 #import "DOMWrapperWorld.h"
35 #import "FloatConversion.h"
36 #import "HTMLMediaElement.h"
37 #import "KURL.h"
38 #import "Language.h"
39 #import "LocalizedStrings.h"
40 #import "Logging.h"
41 #import "MediaControlElements.h"
42 #import "PageGroup.h"
43 #import "SoftLinking.h"
44 #import "TextTrackCue.h"
45 #import "TextTrackList.h"
46 #import "UserStyleSheetTypes.h"
47 #import <wtf/NonCopyingSort.h>
48 #import <wtf/RetainPtr.h>
49 #import <wtf/text/StringBuilder.h>
50
51 #if PLATFORM(IOS)
52 #import "WebCoreThreadRun.h"
53 #endif
54
55 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
56 #import "MediaAccessibility/MediaAccessibility.h"
57 #endif
58
59 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
60
61 SOFT_LINK_FRAMEWORK_OPTIONAL(MediaAccessibility)
62
63 SOFT_LINK(MediaAccessibility, MACaptionAppearanceGetDisplayType, MACaptionAppearanceDisplayType, (MACaptionAppearanceDomain domain), (domain))
64 SOFT_LINK(MediaAccessibility, MACaptionAppearanceSetDisplayType, void, (MACaptionAppearanceDomain domain, MACaptionAppearanceDisplayType displayType), (domain, displayType))
65 SOFT_LINK(MediaAccessibility, MACaptionAppearanceCopyForegroundColor, CGColorRef, (MACaptionAppearanceDomain domain, MACaptionAppearanceBehavior *behavior), (domain, behavior))
66 SOFT_LINK(MediaAccessibility, MACaptionAppearanceCopyBackgroundColor, CGColorRef, (MACaptionAppearanceDomain domain, MACaptionAppearanceBehavior *behavior), (domain, behavior))
67 SOFT_LINK(MediaAccessibility, MACaptionAppearanceCopyWindowColor, CGColorRef, (MACaptionAppearanceDomain domain, MACaptionAppearanceBehavior *behavior), (domain, behavior))
68 SOFT_LINK(MediaAccessibility, MACaptionAppearanceGetForegroundOpacity, CGFloat, (MACaptionAppearanceDomain domain, MACaptionAppearanceBehavior *behavior), (domain, behavior))
69 SOFT_LINK(MediaAccessibility, MACaptionAppearanceGetBackgroundOpacity, CGFloat, (MACaptionAppearanceDomain domain, MACaptionAppearanceBehavior *behavior), (domain, behavior))
70 SOFT_LINK(MediaAccessibility, MACaptionAppearanceGetWindowOpacity, CGFloat, (MACaptionAppearanceDomain domain, MACaptionAppearanceBehavior *behavior), (domain, behavior))
71 SOFT_LINK(MediaAccessibility, MACaptionAppearanceGetWindowRoundedCornerRadius, CGFloat, (MACaptionAppearanceDomain domain, MACaptionAppearanceBehavior *behavior), (domain, behavior))
72 SOFT_LINK(MediaAccessibility, MACaptionAppearanceCopyFontDescriptorForStyle, CTFontDescriptorRef, (MACaptionAppearanceDomain domain,  MACaptionAppearanceBehavior *behavior, MACaptionAppearanceFontStyle fontStyle), (domain, behavior, fontStyle))
73 SOFT_LINK(MediaAccessibility, MACaptionAppearanceGetRelativeCharacterSize, CGFloat, (MACaptionAppearanceDomain domain, MACaptionAppearanceBehavior *behavior), (domain, behavior))
74 SOFT_LINK(MediaAccessibility, MACaptionAppearanceGetTextEdgeStyle, MACaptionAppearanceTextEdgeStyle, (MACaptionAppearanceDomain domain, MACaptionAppearanceBehavior *behavior), (domain, behavior))
75 SOFT_LINK(MediaAccessibility, MACaptionAppearanceAddSelectedLanguage, bool, (MACaptionAppearanceDomain domain, CFStringRef language), (domain, language));
76 SOFT_LINK(MediaAccessibility, MACaptionAppearanceCopySelectedLanguages, CFArrayRef, (MACaptionAppearanceDomain domain), (domain));
77 SOFT_LINK(MediaAccessibility, MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics,  CFArrayRef, (MACaptionAppearanceDomain domain), (domain));
78
79 SOFT_LINK_POINTER(MediaAccessibility, kMAXCaptionAppearanceSettingsChangedNotification, CFStringRef)
80 #define kMAXCaptionAppearanceSettingsChangedNotification getkMAXCaptionAppearanceSettingsChangedNotification()
81
82 #endif
83
84 using namespace std;
85
86 namespace WebCore {
87
88 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
89 static void userCaptionPreferencesChangedNotificationCallback(CFNotificationCenterRef, void* observer, CFStringRef, const void *, CFDictionaryRef)
90 {
91 #if !PLATFORM(IOS)
92     static_cast<CaptionUserPreferencesMac*>(observer)->captionPreferencesChanged();
93 #else
94     WebThreadRun(^{
95         static_cast<CaptionUserPreferencesMac*>(observer)->captionPreferencesChanged();
96     });
97 #endif
98 }
99 #endif
100
101 CaptionUserPreferencesMac::CaptionUserPreferencesMac(PageGroup* group)
102     : CaptionUserPreferences(group)
103 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
104     , m_listeningForPreferenceChanges(false)
105 #endif
106 {
107 }
108
109 CaptionUserPreferencesMac::~CaptionUserPreferencesMac()
110 {
111 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
112     if (kMAXCaptionAppearanceSettingsChangedNotification)
113         CFNotificationCenterRemoveObserver(CFNotificationCenterGetLocalCenter(), this, kMAXCaptionAppearanceSettingsChangedNotification, NULL);
114 #endif
115 }
116
117 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
118
119 CaptionUserPreferences::CaptionDisplayMode CaptionUserPreferencesMac::captionDisplayMode() const
120 {
121     if (testingMode() || !MediaAccessibilityLibrary())
122         return CaptionUserPreferences::captionDisplayMode();
123
124     MACaptionAppearanceDisplayType displayType = MACaptionAppearanceGetDisplayType(kMACaptionAppearanceDomainUser);
125     switch (displayType) {
126     case kMACaptionAppearanceDisplayTypeForcedOnly:
127         return ForcedOnly;
128         break;
129
130     case kMACaptionAppearanceDisplayTypeAutomatic:
131         return Automatic;
132         break;
133
134     case kMACaptionAppearanceDisplayTypeAlwaysOn:
135         return AlwaysOn;
136         break;
137     }
138
139     ASSERT_NOT_REACHED();
140     return ForcedOnly;
141 }
142     
143 void CaptionUserPreferencesMac::setCaptionDisplayMode(CaptionUserPreferences::CaptionDisplayMode mode)
144 {
145     if (testingMode() || !MediaAccessibilityLibrary()) {
146         CaptionUserPreferences::setCaptionDisplayMode(mode);
147         return;
148     }
149
150     MACaptionAppearanceDisplayType displayType = kMACaptionAppearanceDisplayTypeForcedOnly;
151     switch (mode) {
152         case Automatic:
153             displayType = kMACaptionAppearanceDisplayTypeAutomatic;
154             break;
155         case ForcedOnly:
156             displayType = kMACaptionAppearanceDisplayTypeForcedOnly;
157             break;
158         case AlwaysOn:
159             displayType = kMACaptionAppearanceDisplayTypeAlwaysOn;
160             break;
161         default:
162             ASSERT_NOT_REACHED();
163             break;
164     }
165
166     MACaptionAppearanceSetDisplayType(kMACaptionAppearanceDomainUser, displayType);
167 }
168
169 bool CaptionUserPreferencesMac::userPrefersCaptions() const
170 {
171     bool captionSetting = CaptionUserPreferences::userPrefersCaptions();
172     if (captionSetting || testingMode() || !MediaAccessibilityLibrary())
173         return captionSetting;
174     
175     RetainPtr<CFArrayRef> captioningMediaCharacteristics = adoptCF(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser));
176     return captioningMediaCharacteristics && CFArrayGetCount(captioningMediaCharacteristics.get());
177 }
178
179 bool CaptionUserPreferencesMac::userPrefersSubtitles() const
180 {
181     bool subtitlesSetting = CaptionUserPreferences::userPrefersSubtitles();
182     if (subtitlesSetting || testingMode() || !MediaAccessibilityLibrary())
183         return subtitlesSetting;
184     
185     RetainPtr<CFArrayRef> captioningMediaCharacteristics = adoptCF(MACaptionAppearanceCopyPreferredCaptioningMediaCharacteristics(kMACaptionAppearanceDomainUser));
186     return !(captioningMediaCharacteristics && CFArrayGetCount(captioningMediaCharacteristics.get()));
187 }
188
189 void CaptionUserPreferencesMac::setInterestedInCaptionPreferenceChanges()
190 {
191     if (!MediaAccessibilityLibrary())
192         return;
193
194     if (!kMAXCaptionAppearanceSettingsChangedNotification)
195         return;
196
197     if (!m_listeningForPreferenceChanges) {
198         m_listeningForPreferenceChanges = true;
199         CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(), this, userCaptionPreferencesChangedNotificationCallback, kMAXCaptionAppearanceSettingsChangedNotification, NULL, CFNotificationSuspensionBehaviorCoalesce);
200     }
201     
202     updateCaptionStyleSheetOveride();
203 }
204
205 void CaptionUserPreferencesMac::captionPreferencesChanged()
206 {
207     if (m_listeningForPreferenceChanges)
208         updateCaptionStyleSheetOveride();
209
210     CaptionUserPreferences::captionPreferencesChanged();
211 }
212
213 String CaptionUserPreferencesMac::captionsWindowCSS() const
214 {
215     MACaptionAppearanceBehavior behavior;
216     RetainPtr<CGColorRef> color = adoptCF(MACaptionAppearanceCopyWindowColor(kMACaptionAppearanceDomainUser, &behavior));
217
218     Color windowColor(color.get());
219     if (!windowColor.isValid())
220         windowColor = Color::transparent;
221
222     bool important = behavior == kMACaptionAppearanceBehaviorUseValue;
223     CGFloat opacity = MACaptionAppearanceGetWindowOpacity(kMACaptionAppearanceDomainUser, &behavior);
224     if (!important)
225         important = behavior == kMACaptionAppearanceBehaviorUseValue;
226     String windowStyle = colorPropertyCSS(CSSPropertyBackgroundColor, Color(windowColor.red(), windowColor.green(), windowColor.blue(), static_cast<int>(opacity * 255)), important);
227
228     if (!opacity)
229         return windowStyle;
230
231     StringBuilder builder;
232     builder.append(windowStyle);
233     builder.append(getPropertyNameString(CSSPropertyPadding));
234     builder.append(": .4em !important;");
235
236     return builder.toString();
237 }
238
239 String CaptionUserPreferencesMac::captionsBackgroundCSS() const
240 {
241     // This default value must be the same as the one specified in mediaControls.css for -webkit-media-text-track-past-nodes
242     // and webkit-media-text-track-future-nodes.
243     DEFINE_STATIC_LOCAL(Color, defaultBackgroundColor, (Color(0, 0, 0, 0.8 * 255)));
244
245     MACaptionAppearanceBehavior behavior;
246
247     RetainPtr<CGColorRef> color = adoptCF(MACaptionAppearanceCopyBackgroundColor(kMACaptionAppearanceDomainUser, &behavior));
248     Color backgroundColor(color.get());
249     if (!backgroundColor.isValid())
250         backgroundColor = defaultBackgroundColor;
251
252     bool important = behavior == kMACaptionAppearanceBehaviorUseValue;
253     CGFloat opacity = MACaptionAppearanceGetBackgroundOpacity(kMACaptionAppearanceDomainUser, &behavior);
254     if (!important)
255         important = behavior == kMACaptionAppearanceBehaviorUseValue;
256     return colorPropertyCSS(CSSPropertyBackgroundColor, Color(backgroundColor.red(), backgroundColor.green(), backgroundColor.blue(), static_cast<int>(opacity * 255)), important);
257 }
258
259 Color CaptionUserPreferencesMac::captionsTextColor(bool& important) const
260 {
261     MACaptionAppearanceBehavior behavior;
262     RetainPtr<CGColorRef> color = adoptCF(MACaptionAppearanceCopyForegroundColor(kMACaptionAppearanceDomainUser, &behavior));
263     Color textColor(color.get());
264     if (!textColor.isValid())
265         // This default value must be the same as the one specified in mediaControls.css for -webkit-media-text-track-container.
266         textColor = Color::white;
267     
268     important = behavior == kMACaptionAppearanceBehaviorUseValue;
269     CGFloat opacity = MACaptionAppearanceGetForegroundOpacity(kMACaptionAppearanceDomainUser, &behavior);
270     if (!important)
271         important = behavior == kMACaptionAppearanceBehaviorUseValue;
272     return Color(textColor.red(), textColor.green(), textColor.blue(), static_cast<int>(opacity * 255));
273 }
274     
275 String CaptionUserPreferencesMac::captionsTextColorCSS() const
276 {
277     bool important;
278     Color textColor = captionsTextColor(important);
279
280     if (!textColor.isValid())
281         return emptyString();
282
283     return colorPropertyCSS(CSSPropertyColor, textColor, important);
284 }
285     
286 String CaptionUserPreferencesMac::windowRoundedCornerRadiusCSS() const
287 {
288     MACaptionAppearanceBehavior behavior;
289     CGFloat radius = MACaptionAppearanceGetWindowRoundedCornerRadius(kMACaptionAppearanceDomainUser, &behavior);
290     if (!radius)
291         return emptyString();
292
293     StringBuilder builder;
294     builder.append(getPropertyNameString(CSSPropertyBorderRadius));
295     builder.append(String::format(":%.02fpx", radius));
296     if (behavior == kMACaptionAppearanceBehaviorUseValue)
297         builder.append(" !important");
298     builder.append(';');
299
300     return builder.toString();
301 }
302     
303 Color CaptionUserPreferencesMac::captionsEdgeColorForTextColor(const Color& textColor) const
304 {
305     int distanceFromWhite = differenceSquared(textColor, Color::white);
306     int distanceFromBlack = differenceSquared(textColor, Color::black);
307     
308     if (distanceFromWhite < distanceFromBlack)
309         return textColor.dark();
310     
311     return textColor.light();
312 }
313
314 String CaptionUserPreferencesMac::cssPropertyWithTextEdgeColor(CSSPropertyID id, const String& value, const Color& textColor, bool important) const
315 {
316     StringBuilder builder;
317     
318     builder.append(getPropertyNameString(id));
319     builder.append(':');
320     builder.append(value);
321     builder.append(' ');
322     builder.append(captionsEdgeColorForTextColor(textColor).serialized());
323     if (important)
324         builder.append(" !important");
325     builder.append(';');
326     
327     return builder.toString();
328 }
329
330 String CaptionUserPreferencesMac::colorPropertyCSS(CSSPropertyID id, const Color& color, bool important) const
331 {
332     StringBuilder builder;
333     
334     builder.append(getPropertyNameString(id));
335     builder.append(':');
336     builder.append(color.serialized());
337     if (important)
338         builder.append(" !important");
339     builder.append(';');
340     
341     return builder.toString();
342 }
343
344 String CaptionUserPreferencesMac::captionsTextEdgeCSS() const
345 {
346     DEFINE_STATIC_LOCAL(const String, edgeStyleRaised, (" -.05em -.05em 0 ", String::ConstructFromLiteral));
347     DEFINE_STATIC_LOCAL(const String, edgeStyleDepressed, (" .05em .05em 0 ", String::ConstructFromLiteral));
348     DEFINE_STATIC_LOCAL(const String, edgeStyleDropShadow, (" .075em .075em 0 ", String::ConstructFromLiteral));
349     DEFINE_STATIC_LOCAL(const String, edgeStyleUniform, (" .03em ", String::ConstructFromLiteral));
350
351     bool unused;
352     Color color = captionsTextColor(unused);
353     if (!color.isValid())
354         color.setNamedColor("black");
355     color = captionsEdgeColorForTextColor(color);
356
357     MACaptionAppearanceBehavior behavior;
358     MACaptionAppearanceTextEdgeStyle textEdgeStyle = MACaptionAppearanceGetTextEdgeStyle(kMACaptionAppearanceDomainUser, &behavior);
359     switch (textEdgeStyle) {
360         case kMACaptionAppearanceTextEdgeStyleUndefined:
361         case kMACaptionAppearanceTextEdgeStyleNone:
362             return emptyString();
363             
364         case kMACaptionAppearanceTextEdgeStyleRaised:
365             return cssPropertyWithTextEdgeColor(CSSPropertyTextShadow, edgeStyleRaised, color, behavior == kMACaptionAppearanceBehaviorUseValue);
366         case kMACaptionAppearanceTextEdgeStyleDepressed:
367             return cssPropertyWithTextEdgeColor(CSSPropertyTextShadow, edgeStyleDepressed, color, behavior == kMACaptionAppearanceBehaviorUseValue);
368         case kMACaptionAppearanceTextEdgeStyleDropShadow:
369             return cssPropertyWithTextEdgeColor(CSSPropertyTextShadow, edgeStyleDropShadow, color, behavior == kMACaptionAppearanceBehaviorUseValue);
370         case kMACaptionAppearanceTextEdgeStyleUniform:
371             return cssPropertyWithTextEdgeColor(CSSPropertyWebkitTextStroke, edgeStyleUniform, color, behavior == kMACaptionAppearanceBehaviorUseValue);
372             
373         default:
374             ASSERT_NOT_REACHED();
375             break;
376     }
377     
378     return emptyString();
379 }
380
381 String CaptionUserPreferencesMac::captionsDefaultFontCSS() const
382 {
383     MACaptionAppearanceBehavior behavior;
384     
385     RetainPtr<CTFontDescriptorRef> font = adoptCF(MACaptionAppearanceCopyFontDescriptorForStyle(kMACaptionAppearanceDomainUser, &behavior, kMACaptionAppearanceFontStyleDefault));
386     if (!font)
387         return emptyString();
388
389     RetainPtr<CFTypeRef> name = adoptCF(CTFontDescriptorCopyAttribute(font.get(), kCTFontNameAttribute));
390     if (!name)
391         return emptyString();
392     
393     StringBuilder builder;
394     
395     builder.append(getPropertyNameString(CSSPropertyFontFamily));
396     builder.append(": \"");
397     builder.append(static_cast<CFStringRef>(name.get()));
398     builder.append('"');
399     if (behavior == kMACaptionAppearanceBehaviorUseValue)
400         builder.append(" !important");
401     builder.append(';');
402     
403     return builder.toString();
404 }
405
406 float CaptionUserPreferencesMac::captionFontSizeScaleAndImportance(bool& important) const
407 {
408     if (testingMode() || !MediaAccessibilityLibrary())
409         return CaptionUserPreferences::captionFontSizeScaleAndImportance(important);
410
411     MACaptionAppearanceBehavior behavior;
412     CGFloat characterScale = CaptionUserPreferences::captionFontSizeScaleAndImportance(important);
413     CGFloat scaleAdjustment = MACaptionAppearanceGetRelativeCharacterSize(kMACaptionAppearanceDomainUser, &behavior);
414
415     if (!scaleAdjustment)
416         return characterScale;
417
418     important = behavior == kMACaptionAppearanceBehaviorUseValue;
419 #if defined(__LP64__) && __LP64__
420     return narrowPrecisionToFloat(scaleAdjustment * characterScale);
421 #else
422     return scaleAdjustment * characterScale;
423 #endif
424 }
425
426 void CaptionUserPreferencesMac::setPreferredLanguage(const String& language)
427 {
428     if (testingMode() || !MediaAccessibilityLibrary()) {
429         CaptionUserPreferences::setPreferredLanguage(language);
430         return;
431     }
432
433     MACaptionAppearanceAddSelectedLanguage(kMACaptionAppearanceDomainUser, language.createCFString().get());
434 }
435
436 Vector<String> CaptionUserPreferencesMac::preferredLanguages() const
437 {
438     if (testingMode() || !MediaAccessibilityLibrary())
439         return CaptionUserPreferences::preferredLanguages();
440
441     Vector<String> platformLanguages = platformUserPreferredLanguages();
442     Vector<String> override = userPreferredLanguagesOverride();
443     if (!override.isEmpty()) {
444         if (platformLanguages.size() != override.size())
445             return override;
446         for (size_t i = 0; i < override.size(); i++) {
447             if (override[i] != platformLanguages[i])
448                 return override;
449         }
450     }
451
452     CFIndex languageCount = 0;
453     RetainPtr<CFArrayRef> languages = adoptCF(MACaptionAppearanceCopySelectedLanguages(kMACaptionAppearanceDomainUser));
454     if (languages)
455         languageCount = CFArrayGetCount(languages.get());
456
457     if (!languageCount)
458         return CaptionUserPreferences::preferredLanguages();
459
460     Vector<String> userPreferredLanguages;
461     userPreferredLanguages.reserveCapacity(languageCount + platformLanguages.size());
462     for (CFIndex i = 0; i < languageCount; i++)
463         userPreferredLanguages.append(static_cast<CFStringRef>(CFArrayGetValueAtIndex(languages.get(), i)));
464
465     userPreferredLanguages.appendVector(platformLanguages);
466
467     return userPreferredLanguages;
468 }
469 #endif  // HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
470
471 String CaptionUserPreferencesMac::captionsStyleSheetOverride() const
472 {
473     if (testingMode())
474         return CaptionUserPreferences::captionsStyleSheetOverride();
475     
476     StringBuilder captionsOverrideStyleSheet;
477
478 #if HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
479     if (!MediaAccessibilityLibrary())
480         return CaptionUserPreferences::captionsStyleSheetOverride();
481     
482     String captionsColor = captionsTextColorCSS();
483     String edgeStyle = captionsTextEdgeCSS();
484     String fontName = captionsDefaultFontCSS();
485     String background = captionsBackgroundCSS();
486     if (!background.isEmpty() || !captionsColor.isEmpty() || !edgeStyle.isEmpty() || !fontName.isEmpty()) {
487         captionsOverrideStyleSheet.append(" video::");
488         captionsOverrideStyleSheet.append(TextTrackCue::cueShadowPseudoId());
489         captionsOverrideStyleSheet.append('{');
490         
491         if (!background.isEmpty())
492             captionsOverrideStyleSheet.append(background);
493         if (!captionsColor.isEmpty())
494             captionsOverrideStyleSheet.append(captionsColor);
495         if (!edgeStyle.isEmpty())
496             captionsOverrideStyleSheet.append(edgeStyle);
497         if (!fontName.isEmpty())
498             captionsOverrideStyleSheet.append(fontName);
499         
500         captionsOverrideStyleSheet.append('}');
501     }
502     
503     String windowColor = captionsWindowCSS();
504     String windowCornerRadius = windowRoundedCornerRadiusCSS();
505     if (!windowColor.isEmpty() || !windowCornerRadius.isEmpty()) {
506         captionsOverrideStyleSheet.append(" video::");
507         captionsOverrideStyleSheet.append(TextTrackCueBox::textTrackCueBoxShadowPseudoId());
508         captionsOverrideStyleSheet.append('{');
509         
510         if (!windowColor.isEmpty())
511             captionsOverrideStyleSheet.append(windowColor);
512         if (!windowCornerRadius.isEmpty())
513             captionsOverrideStyleSheet.append(windowCornerRadius);
514         
515         captionsOverrideStyleSheet.append('}');
516     }
517 #endif  // HAVE(MEDIA_ACCESSIBILITY_FRAMEWORK)
518
519     LOG(Media, "CaptionUserPreferencesMac::captionsStyleSheetOverrideSetting sytle to:\n%s", captionsOverrideStyleSheet.toString().utf8().data());
520
521     return captionsOverrideStyleSheet.toString();
522 }
523
524 static String languageIdentifier(const String& languageCode)
525 {
526     if (languageCode.isEmpty())
527         return languageCode;
528
529     String lowercaseLanguageCode = languageCode.lower();
530
531     // Need 2U here to disambiguate String::operator[] from operator(NSString*, int)[] in a production build.
532     if (lowercaseLanguageCode.length() >= 3 && (lowercaseLanguageCode[2U] == '_' || lowercaseLanguageCode[2U] == '-'))
533         lowercaseLanguageCode.truncate(2);
534
535     return lowercaseLanguageCode;
536 }
537
538 static String trackDisplayName(TextTrack* track)
539 {
540     if (track == TextTrack::captionMenuOffItem())
541         return textTrackOffMenuItemText();
542     if (track == TextTrack::captionMenuAutomaticItem())
543         return textTrackAutomaticMenuItemText();
544
545     StringBuilder displayName;
546     String label = track->label();
547     String trackLanguageIdentifier = track->language();
548
549     RetainPtr<CFLocaleRef> currentLocale = adoptCF(CFLocaleCopyCurrent());
550     RetainPtr<CFStringRef> localeIdentifier = adoptCF(CFLocaleCreateCanonicalLocaleIdentifierFromString(kCFAllocatorDefault, trackLanguageIdentifier.createCFString().get()));
551     RetainPtr<CFStringRef> languageCF = adoptCF(CFLocaleCopyDisplayNameForPropertyValue(currentLocale.get(), kCFLocaleLanguageCode, localeIdentifier.get()));
552     String language = languageCF.get();
553     if (!label.isEmpty()) {
554         if (language.isEmpty() || label.contains(language))
555             displayName.append(label);
556         else {
557             RetainPtr<CFDictionaryRef> localeDict = adoptCF(CFLocaleCreateComponentsFromLocaleIdentifier(kCFAllocatorDefault, localeIdentifier.get()));
558             if (localeDict) {
559                 CFStringRef countryCode = 0;
560                 String countryName;
561                 
562                 CFDictionaryGetValueIfPresent(localeDict.get(), kCFLocaleCountryCode, (const void **)&countryCode);
563                 if (countryCode) {
564                     RetainPtr<CFStringRef> countryNameCF = adoptCF(CFLocaleCopyDisplayNameForPropertyValue(currentLocale.get(), kCFLocaleCountryCode, countryCode));
565                     countryName = countryNameCF.get();
566                 }
567                 
568                 if (!countryName.isEmpty())
569                     displayName.append(textTrackCountryAndLanguageMenuItemText(label, countryName, language));
570                 else
571                     displayName.append(textTrackLanguageMenuItemText(label, language));
572             }
573         }
574     } else {
575         String languageAndLocale = CFLocaleCopyDisplayNameForPropertyValue(currentLocale.get(), kCFLocaleIdentifier, trackLanguageIdentifier.createCFString().get());
576         if (!languageAndLocale.isEmpty())
577             displayName.append(languageAndLocale);
578         else if (!language.isEmpty())
579             displayName.append(language);
580         else
581             displayName.append(localeIdentifier.get());
582     }
583     
584     if (displayName.isEmpty())
585         displayName.append(textTrackNoLabelText());
586     
587     if (track->isEasyToRead())
588         return easyReaderTrackMenuItemText(displayName.toString());
589     
590     if (track->isClosedCaptions())
591         return closedCaptionTrackMenuItemText(displayName.toString());
592
593     if (track->isSDH())
594         return sdhTrackMenuItemText(displayName.toString());
595
596     return displayName.toString();
597 }
598
599 String CaptionUserPreferencesMac::displayNameForTrack(TextTrack* track) const
600 {
601     return trackDisplayName(track);
602 }
603
604 int CaptionUserPreferencesMac::textTrackSelectionScore(TextTrack* track, HTMLMediaElement* mediaElement) const
605 {
606     CaptionDisplayMode displayMode = captionDisplayMode();
607     bool legacyOverride = mediaElement->webkitClosedCaptionsVisible();
608     if (displayMode == AlwaysOn && (!userPrefersSubtitles() && !userPrefersCaptions() && !legacyOverride))
609         return 0;
610     if (track->kind() != TextTrack::captionsKeyword() && track->kind() != TextTrack::subtitlesKeyword() && track->kind() != TextTrack::forcedKeyword())
611         return 0;
612     if (!track->isMainProgramContent())
613         return 0;
614
615     bool trackHasOnlyForcedSubtitles = track->containsOnlyForcedSubtitles();
616     if (!legacyOverride && ((trackHasOnlyForcedSubtitles && displayMode != ForcedOnly) || (!trackHasOnlyForcedSubtitles && displayMode == ForcedOnly)))
617         return 0;
618
619     Vector<String> userPreferredCaptionLanguages = preferredLanguages();
620
621     if (displayMode == Automatic || trackHasOnlyForcedSubtitles) {
622
623         if (!mediaElement || !mediaElement->player())
624             return 0;
625
626         String textTrackLanguage = track->language();
627         if (textTrackLanguage.isEmpty())
628             return 0;
629
630         Vector<String> languageList;
631         languageList.reserveCapacity(1);
632
633         String audioTrackLanguage;
634         if (testingMode())
635             audioTrackLanguage = primaryAudioTrackLanguageOverride();
636         else
637             audioTrackLanguage = mediaElement->player()->languageOfPrimaryAudioTrack();
638
639         if (audioTrackLanguage.isEmpty())
640             return 0;
641
642         if (trackHasOnlyForcedSubtitles) {
643             languageList.append(audioTrackLanguage);
644             size_t offset = indexOfBestMatchingLanguageInList(textTrackLanguage, languageList);
645
646             // Only consider a forced-only track if it IS in the same language as the primary audio track.
647             if (offset)
648                 return 0;
649         } else {
650             languageList.append(defaultLanguage());
651
652             // Only enable a text track if the current audio track is NOT in the user's preferred language ...
653             size_t offset = indexOfBestMatchingLanguageInList(audioTrackLanguage, languageList);
654             if (!offset)
655                 return 0;
656
657             // and the text track matches the user's preferred language.
658             offset = indexOfBestMatchingLanguageInList(textTrackLanguage, languageList);
659             if (offset)
660                 return 0;
661         }
662
663         userPreferredCaptionLanguages = languageList;
664     }
665
666     int trackScore = 0;
667
668     if (userPrefersCaptions()) {
669         // When the user prefers accessiblity tracks, rank is SDH, then CC, then subtitles.
670         if (track->kind() == track->subtitlesKeyword())
671             trackScore = 1;
672         else if (track->isClosedCaptions())
673             trackScore = 2;
674         else
675             trackScore = 3;
676     } else {
677         // When the user prefers translation tracks, rank is subtitles, then SDH, then CC tracks.
678         if (track->kind() == track->subtitlesKeyword())
679             trackScore = 3;
680         else if (!track->isClosedCaptions())
681             trackScore = 2;
682         else
683             trackScore = 1;
684     }
685
686     return trackScore + textTrackLanguageSelectionScore(track, userPreferredCaptionLanguages);
687 }
688
689 static bool textTrackCompare(const RefPtr<TextTrack>& a, const RefPtr<TextTrack>& b)
690 {
691     String preferredLanguageDisplayName = displayNameForLanguageLocale(languageIdentifier(defaultLanguage()));
692     String aLanguageDisplayName = displayNameForLanguageLocale(languageIdentifier(a->language()));
693     String bLanguageDisplayName = displayNameForLanguageLocale(languageIdentifier(b->language()));
694
695     // Tracks in the user's preferred language are always at the top of the menu.
696     bool aIsPreferredLanguage = !codePointCompare(aLanguageDisplayName, preferredLanguageDisplayName);
697     bool bIsPreferredLanguage = !codePointCompare(bLanguageDisplayName, preferredLanguageDisplayName);
698     if ((aIsPreferredLanguage || bIsPreferredLanguage) && (aIsPreferredLanguage != bIsPreferredLanguage))
699         return aIsPreferredLanguage;
700
701     // Tracks not in the user's preferred language sort first by language ...
702     if (codePointCompare(aLanguageDisplayName, bLanguageDisplayName))
703         return codePointCompare(aLanguageDisplayName, bLanguageDisplayName) < 0;
704
705     // ... but when tracks have the same language, main program content sorts next highest ...
706     bool aIsMainContent = a->isMainProgramContent();
707     bool bIsMainContent = b->isMainProgramContent();
708     if ((aIsMainContent || bIsMainContent) && (aIsMainContent != bIsMainContent))
709         return aIsMainContent;
710
711     // ... and main program trakcs sort higher than CC tracks ...
712     bool aIsCC = a->isClosedCaptions();
713     bool bIsCC = b->isClosedCaptions();
714     if ((aIsCC || bIsCC) && (aIsCC != bIsCC)) {
715         if (aIsCC)
716             return aIsMainContent;
717         return bIsMainContent;
718     }
719
720     // ... and tracks of the same type and language sort by the menu item text.
721     return codePointCompare(trackDisplayName(a.get()), trackDisplayName(b.get())) < 0;
722 }
723
724 Vector<RefPtr<TextTrack> > CaptionUserPreferencesMac::sortedTrackListForMenu(TextTrackList* trackList)
725 {
726     ASSERT(trackList);
727
728     Vector<RefPtr<TextTrack> > tracksForMenu;
729     HashSet<String> languagesIncluded;
730     bool prefersAccessibilityTracks = userPrefersCaptions();
731     bool filterTrackList = shouldFilterTrackMenu();
732
733     for (unsigned i = 0, length = trackList->length(); i < length; ++i) {
734         TextTrack* track = trackList->item(i);
735         String language = displayNameForLanguageLocale(track->language());
736
737         if (track->containsOnlyForcedSubtitles())
738             continue;
739         
740         if (track->isEasyToRead()) {
741             if (!language.isEmpty())
742                 languagesIncluded.add(language);
743             tracksForMenu.append(track);
744             continue;
745         }
746
747         if (!language.isEmpty() && track->isMainProgramContent()) {
748             bool isAccessibilityTrack = track->kind() == track->captionsKeyword();
749             if (prefersAccessibilityTracks) {
750                 // In the first pass, include only caption tracks if the user prefers accessibility tracks.
751                 if (!isAccessibilityTrack && filterTrackList) {
752                     LOG(Media, "CaptionUserPreferencesMac::sortedTrackListForMenu - skipping '%s' track with language '%s' because it is NOT an accessibility track", track->kind().string().utf8().data(), language.utf8().data());
753                     continue;
754                 }
755             } else {
756                 // In the first pass, only include the first non-CC or SDH track with each language if the user prefers translation tracks.
757                 if (isAccessibilityTrack && filterTrackList) {
758                     LOG(Media, "CaptionUserPreferencesMac::sortedTrackListForMenu - skipping '%s' track with language '%s' because it is an accessibility track", track->kind().string().utf8().data(), language.utf8().data());
759                     continue;
760                 }
761                 if (languagesIncluded.contains(language)  && filterTrackList) {
762                     LOG(Media, "CaptionUserPreferencesMac::sortedTrackListForMenu - skipping '%s' track with language '%s' because it is not the first with this language", track->kind().string().utf8().data(), language.utf8().data());
763                     continue;
764                 }
765             }
766         }
767
768         if (!language.isEmpty())
769             languagesIncluded.add(language);
770         tracksForMenu.append(track);
771     }
772
773     // Now that we have filtered for the user's accessibility/translation preference, add  all tracks with a unique language without regard to track type.
774     for (unsigned i = 0, length = trackList->length(); i < length; ++i) {
775         TextTrack* track = trackList->item(i);
776         String language = displayNameForLanguageLocale(track->language());
777
778         // All candidates with no languge were added the first time through.
779         if (language.isEmpty())
780             continue;
781
782         if (track->containsOnlyForcedSubtitles())
783             continue;
784  
785         if (!languagesIncluded.contains(language) && track->isMainProgramContent()) {
786             languagesIncluded.add(language);
787             tracksForMenu.append(track);
788             LOG(Media, "CaptionUserPreferencesMac::sortedTrackListForMenu - adding '%s' track with language '%s' because it is the only track with this language", track->kind().string().utf8().data(), language.utf8().data());
789         }
790     }
791
792     nonCopyingSort(tracksForMenu.begin(), tracksForMenu.end(), textTrackCompare);
793
794     tracksForMenu.insert(0, TextTrack::captionMenuOffItem());
795     tracksForMenu.insert(1, TextTrack::captionMenuAutomaticItem());
796
797     return tracksForMenu;
798 }
799     
800 }
801
802 #endif // ENABLE(VIDEO_TRACK)