| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview 'tts-subpage' is the collapsible section containing |
| * text-to-speech settings. |
| */ |
| |
| Polymer({ |
| is: 'settings-tts-subpage', |
| |
| behaviors: [WebUIListenerBehavior, I18nBehavior], |
| |
| properties: { |
| /** |
| * Preferences state. |
| */ |
| prefs: { |
| type: Object, |
| notify: true, |
| }, |
| |
| /** |
| * Available languages. |
| * @type {Array<{language: string, code: string, preferred: boolean, |
| * voice: TtsHandlerVoice}>} |
| */ |
| languagesToVoices: { |
| type: Array, |
| notify: true, |
| }, |
| |
| /** |
| * All voices. |
| * @type {Array<TtsHandlerVoice>} |
| */ |
| allVoices: { |
| type: Array, |
| value: [], |
| notify: true, |
| }, |
| |
| /** |
| * Default preview voice. |
| */ |
| defaultPreviewVoice: { |
| type: String, |
| notify: true, |
| }, |
| |
| /** |
| * Whether preview is currently speaking. |
| * @private |
| */ |
| isPreviewing_: { |
| type: Boolean, |
| value: false, |
| }, |
| |
| /** Whether any voices are loaded. */ |
| hasVoices: { |
| type: Boolean, |
| computed: 'hasVoices_(allVoices)', |
| }, |
| |
| /** Whether the additional languages section has been opened. */ |
| languagesOpened: { |
| type: Boolean, |
| value: false, |
| }, |
| }, |
| |
| /** @override */ |
| ready: function() { |
| this.addWebUIListener( |
| 'all-voice-data-updated', this.populateVoiceList_.bind(this)); |
| chrome.send('getAllTtsVoiceData'); |
| this.addWebUIListener( |
| 'tts-extensions-updated', this.populateExtensionList_.bind(this)); |
| this.addWebUIListener( |
| 'tts-preview-state-changed', this.onTtsPreviewStateChanged_.bind(this)); |
| chrome.send('getTtsExtensions'); |
| }, |
| |
| /** |
| * Ticks for the Speech Rate slider. Non-linear as we expect people |
| * to want more control near 1.0. |
| * @return Array<cr_slider.SliderTick> |
| * @private |
| */ |
| speechRateTicks_: function() { |
| return Array.from(Array(16).keys()).map(x => { |
| return x <= 4 ? |
| // Linear from rates 0.6 to 1.0 |
| this.initTick_(x / 10 + .6) : |
| // Power function above 1.0 gives more control at lower values. |
| this.initTick_(Math.pow(x - 3, 2) / 20 + 1); |
| }); |
| }, |
| |
| /** |
| * Ticks for the Speech Pitch slider. Valid pitches are between 0 and 2, |
| * exclusive of 0. |
| * @return Array<cr_slider.SliderTick> |
| * @private |
| */ |
| speechPitchTicks_: function() { |
| return Array.from(Array(10).keys()).map(x => { |
| return this.initTick_(x * .2 + .2); |
| }); |
| }, |
| |
| /** |
| * Ticks for the Speech Volume slider. Valid volumes are between 0 and |
| * 1 (100%), but volumes lower than .2 are excluded as being too quiet. |
| * The values are linear between .2 and 1.0. |
| * @return Array<cr_slider.SliderTick> |
| * @private |
| */ |
| speechVolumeTicks_: function() { |
| return Array.from(Array(9).keys()).map(x => { |
| return this.initTick_(x * .1 + .2); |
| }); |
| }, |
| |
| /** |
| * Initializes i18n labels for ticks arrays. |
| * @param {number} tick The value to make a tick for. |
| * @return {cr_slider.SliderTick} |
| * @private |
| */ |
| initTick_: function(tick) { |
| const value = Math.round(100 * tick); |
| const strValue = value.toFixed(0); |
| const label = strValue === '100' ? |
| this.i18n('defaultPercentage', strValue) : |
| this.i18n('percentage', strValue); |
| return {label: label, value: tick, ariaValue: value}; |
| }, |
| |
| /** |
| * Returns true if any voices are loaded. |
| * @param {!Array<TtsHandlerVoice>} voices |
| * @return {boolean} |
| * @private |
| */ |
| hasVoices_: function(voices) { |
| return voices.length > 0; |
| }, |
| |
| /** |
| * Returns true if voices are loaded and preview is not currently speaking. |
| * @param {!Array<TtsHandlerVoice>} voices |
| * @param {boolean} isPreviewing |
| * @return {boolean} |
| * @private |
| */ |
| enablePreviewButton_: function(voices, isPreviewing) { |
| return this.hasVoices_(voices) && !isPreviewing; |
| }, |
| |
| /** |
| * Populates the list of languages and voices for the UI to use in display. |
| * @param {Array<TtsHandlerVoice>} voices |
| * @private |
| */ |
| populateVoiceList_: function(voices) { |
| // Build a map of language code to human-readable language and voice. |
| const result = {}; |
| const languageCodeMap = {}; |
| const pref = this.prefs.settings['language']['preferred_languages']; |
| const preferredLangs = pref.value.split(','); |
| voices.forEach(voice => { |
| if (!result[voice.languageCode]) { |
| result[voice.languageCode] = { |
| language: voice.displayLanguage, |
| code: voice.languageCode, |
| preferred: false, |
| voices: [] |
| }; |
| } |
| // Each voice gets a unique ID from its name and extension. |
| voice.id = |
| JSON.stringify({name: voice.name, extension: voice.extensionId}); |
| // TODO(katie): Make voices a map rather than an array to enforce |
| // uniqueness, then convert back to an array for polymer repeat. |
| result[voice.languageCode].voices.push(voice); |
| |
| // A language is "preferred" if it has a voice that uses the default |
| // locale of the device. |
| result[voice.languageCode].preferred = |
| result[voice.languageCode].preferred || |
| preferredLangs.indexOf(voice.fullLanguageCode) != -1; |
| languageCodeMap[voice.fullLanguageCode] = voice.languageCode; |
| }); |
| this.updateLangToVoicePrefs_(result); |
| this.set('languagesToVoices', Object.values(result)); |
| this.set('allVoices', voices); |
| this.setDefaultPreviewVoiceForLocale_(voices, languageCodeMap); |
| }, |
| |
| /** |
| * Returns true if the language is a primary language and should be shown by |
| * default, false if it should be hidden by default. |
| * @param {{language: string, code: string, preferred: boolean, |
| * voice: TtsHandlerVoice}} language |
| * @return {boolean} true if it's a primary language. |
| */ |
| isPrimaryLanguage_: function(language) { |
| return language.preferred; |
| }, |
| |
| /** |
| * Returns true if the language is a secondary language and should be hidden |
| * by default, true if it should be shown by default. |
| * @param {{language: string, code: string, preferred: boolean, |
| * voice: TtsHandlerVoice}} language |
| * @return {boolean} true if it's a secondary language. |
| */ |
| isSecondaryLanguage_: function(language) { |
| return !language.preferred; |
| }, |
| |
| /** |
| * Sets the list of Text-to-Speech extensions for the UI. |
| * @param {Array<TtsHandlerExtension>} extensions |
| * @private |
| */ |
| populateExtensionList_: function(extensions) { |
| this.extensions = extensions; |
| }, |
| |
| /** |
| * Called when the TTS voice preview state changes between speaking and not |
| * speaking. |
| * @param {boolean} isSpeaking |
| * @private |
| */ |
| onTtsPreviewStateChanged_: function(isSpeaking) { |
| this.isPreviewing_ = isSpeaking; |
| }, |
| |
| /** |
| * A function used for sorting languages alphabetically. |
| * @param {Object} first A languageToVoices array item. |
| * @param {Object} second A languageToVoices array item. |
| * @return {number} The result of the comparison. |
| * @private |
| */ |
| alphabeticalSort_: function(first, second) { |
| return first.language.localeCompare(second.language); |
| }, |
| |
| /** |
| * Tests whether a language has just once voice. |
| * @param {Object} lang A languageToVoices array item. |
| * @return {boolean} True if the item has only one voice. |
| * @private |
| */ |
| hasOneLanguage_: function(lang) { |
| return lang['voices'].length == 1; |
| }, |
| |
| /** |
| * Returns a list of objects that can be used as drop-down menu options for a |
| * language. This is a list of voices in that language. |
| * @param {Object} lang A languageToVoices array item. |
| * @return {Array<Object>} An array of menu options with a value and name. |
| * @private |
| */ |
| menuOptionsForLang_: function(lang) { |
| return lang.voices.map(voice => { |
| return {value: voice.id, name: voice.name}; |
| }); |
| }, |
| |
| /** |
| * Updates the preferences given the current list of voices. |
| * @param {Object<string, {language: string, code: string, preferred: boolean, |
| * voices: Array<TtsHandlerVoice>}>} langToVoices |
| * @private |
| */ |
| updateLangToVoicePrefs_: function(langToVoices) { |
| if (langToVoices.length == 0) |
| return; |
| const allCodes = new Set( |
| Object.keys(this.prefs.settings['tts']['lang_to_voice_name'].value)); |
| for (const code in langToVoices) { |
| // Remove from allCodes, to track what we've found a default for. |
| allCodes.delete(code); |
| const voices = langToVoices[code].voices; |
| const defaultVoiceForLang = |
| this.prefs.settings['tts']['lang_to_voice_name'].value[code]; |
| if (!defaultVoiceForLang || defaultVoiceForLang === '') { |
| // Initialize prefs that have no value |
| this.set( |
| 'prefs.settings.tts.lang_to_voice_name.value.' + code, |
| this.getBestVoiceForLocale_(voices)); |
| continue; |
| } |
| // See if the set voice ID is in the voices list, in which case we are |
| // done checking this language. |
| if (voices.some(voice => voice.id === defaultVoiceForLang)) |
| continue; |
| // Change prefs that point to voices that no longer exist. |
| this.set( |
| 'prefs.settings.tts.lang_to_voice_name.value.' + code, |
| this.getBestVoiceForLocale_(voices)); |
| } |
| // If there are any items left in allCodes, they are for languages that are |
| // no longer covered by the UI. We could now delete them from the |
| // lang_to_voice_name pref. |
| for (const code of allCodes) { |
| this.set('prefs.settings.tts.lang_to_voice_name.value.' + code, ''); |
| } |
| }, |
| |
| /** |
| * Sets the voice to show in the preview drop-down as default, based on the |
| * current locale and voice preferences. |
| * @param {Array<TtsHandlerVoice>} allVoices |
| * @param {Object<string, string>} languageCodeMap Mapping from language code |
| * to simple language code without locale. |
| * @private |
| */ |
| setDefaultPreviewVoiceForLocale_: function(allVoices, languageCodeMap) { |
| if (!allVoices || allVoices.length == 0) |
| return; |
| |
| // Force a synchronous render so that we can set the default. |
| this.$.previewVoiceOptions.render(); |
| |
| // Set something if nothing exists. This useful for new users where |
| // sometimes browserProxy.getProspectiveUILanguage() does not complete the |
| // callback. |
| if (!this.defaultPreviewVoice) |
| this.set('defaultPreviewVoice', this.getBestVoiceForLocale_(allVoices)); |
| |
| const browserProxy = settings.LanguagesBrowserProxyImpl.getInstance(); |
| browserProxy.getProspectiveUILanguage().then(prospectiveUILanguage => { |
| let result; |
| if (prospectiveUILanguage && prospectiveUILanguage != '' && |
| languageCodeMap[prospectiveUILanguage]) { |
| const code = languageCodeMap[prospectiveUILanguage]; |
| // First try the pref value. |
| result = this.prefs.settings['tts']['lang_to_voice_name'].value[code]; |
| } |
| if (!result) { |
| // If it's not a pref value yet, or the prospectiveUILanguage was |
| // missing, try using the voice score. |
| result = this.getBestVoiceForLocale_(allVoices); |
| } |
| this.set('defaultPreviewVoice', result); |
| }); |
| }, |
| |
| /** |
| * Gets the best voice for the app locale. |
| * @param {Array<TtsHandlerVoice>} voices Voices to search through. |
| * @return {string} The ID of the best matching voice in the array. |
| * @private |
| */ |
| getBestVoiceForLocale_: function(voices) { |
| let bestScore = -1; |
| let bestVoice = ''; |
| voices.forEach((voice) => { |
| if (voice.languageScore > bestScore) { |
| bestScore = voice.languageScore; |
| bestVoice = voice.id; |
| } |
| }); |
| return bestVoice; |
| }, |
| |
| /** @private */ |
| onPreviewTtsClick_: function() { |
| chrome.send( |
| 'previewTtsVoice', |
| [this.$.previewInput.value, this.$.previewVoice.value]); |
| chrome.metricsPrivate.recordSparseHashable( |
| 'TextToSpeech.Settings.PreviewVoiceClicked', this.$.previewVoice.value); |
| }, |
| |
| /** @private */ |
| onDefaultTtsVoicePicked_: function(event) { |
| // Log the default voice the user selected. Each voice has at most one |
| // language, so there's no need to log language as well. |
| // The event target is the settings-dropdown-menu. |
| const target = /** @type {{prefStringValue_: function():string}} */ |
| (event.target); |
| const newDefault = target.prefStringValue_(); |
| chrome.metricsPrivate.recordSparseHashable( |
| 'TextToSpeech.Settings.DefaultVoicePicked', newDefault); |
| }, |
| |
| /** |
| * @param {{model:Object}} event |
| * @private |
| */ |
| onEngineSettingsTap_: function(event) { |
| chrome.send('wakeTtsEngine'); |
| window.open(event.model.extension.optionsPage); |
| }, |
| }); |