Commit 90e6d701 authored by Katie D's avatar Katie D Committed by Commit Bot

Text-to-Speech settings to pick default voice per language.

Because language -> voice pref is a DICTONARY type pref, this
change extends settings-dropdown-menu to work with dictionary
prefs given a new parameter, prefKey. When set, prefKey tells
the settings dropdown which key in the dictionary is associated
with the value it represents.

This is not yet hooked up to the Text-to-Speech controller. In
addition, we don't yet define default voices per language.

Bug: 823359
Cq-Include-Trybots: master.tryserver.chromium.linux:closure_compilation
Change-Id: I6b7ae800ed6f0230076da5d65114d305fc1dc64d
Reviewed-on: https://chromium-review.googlesource.com/1020456
Commit-Queue: Katie Dektar <katie@chromium.org>
Reviewed-by: default avatarHector Carmona <hcarmona@chromium.org>
Cr-Commit-Position: refs/heads/master@{#553911}
parent 403310d4
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
'dependencies': [ 'dependencies': [
'externs', 'externs',
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:load_time_data', '<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:load_time_data',
'<(DEPTH)/ui/webui/resources/js/compiled_resources2.gyp:web_ui_listener_behavior',
'../settings_page/compiled_resources2.gyp:settings_animated_pages', '../settings_page/compiled_resources2.gyp:settings_animated_pages',
'../compiled_resources2.gyp:route', '../compiled_resources2.gyp:route',
], ],
......
...@@ -22,3 +22,9 @@ Window.prototype.speechSynthesis.speak = function(utterance) {}; ...@@ -22,3 +22,9 @@ Window.prototype.speechSynthesis.speak = function(utterance) {};
* @constructor * @constructor
*/ */
Window.prototype.SpeechSynthesisUtterance = function() {}; Window.prototype.SpeechSynthesisUtterance = function() {};
/**
* @typedef {{languageCode: string, name: string, displayLanguage: string,
* extensionId: string, id: string}}
*/
let TtsHandlerVoice;
<link rel="import" href="chrome://resources/html/polymer.html"> <link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/html/web_ui_listener_behavior.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-input/paper-textarea.html">
<link rel="import" href="../controls/settings_slider.html"> <link rel="import" href="../controls/settings_slider.html">
<link rel="import" href="../i18n_setup.html"> <link rel="import" href="../i18n_setup.html">
<link rel="import" href="../languages_page/languages_browser_proxy.html">
<link rel="import" href="../settings_shared_css.html"> <link rel="import" href="../settings_shared_css.html">
<link rel="import" href="../settings_vars_css.html"> <link rel="import" href="../settings_vars_css.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-input/paper-textarea.html">
<dom-module id="settings-tts-subpage"> <dom-module id="settings-tts-subpage">
<template> <template>
...@@ -22,8 +24,20 @@ ...@@ -22,8 +24,20 @@
} }
</style> </style>
<h2>$i18n{textToSpeechVoices}</h2> <h2>$i18n{textToSpeechVoices}</h2>
<div class="settings-box block first"> <template is="dom-repeat" items="[[languagesToVoices]]" as="lang"
</div> sort="alphabeticalSort_">
<div class="settings-box first">
<div class="start">
[[lang.language]]
</div>
<settings-dropdown-menu label="[[lang.language]]"
pref="{{prefs.settings.tts.lang_to_voice_name}}"
pref-key="[[lang.code]]"
menu-options="[[menuOptionsForLang_(lang)]]"
disabled="[[hasOneLanguage_(lang)]]">
</settings-dropdown-menu>
</div>
</template>
<h2>$i18n{textToSpeechProperties}</h2> <h2>$i18n{textToSpeechProperties}</h2>
<div class="settings-box first"> <div class="settings-box first">
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
Polymer({ Polymer({
is: 'settings-tts-subpage', is: 'settings-tts-subpage',
behaviors: [WebUIListenerBehavior],
properties: { properties: {
/** /**
* Preferences state. * Preferences state.
...@@ -18,6 +20,82 @@ Polymer({ ...@@ -18,6 +20,82 @@ Polymer({
type: Object, type: Object,
notify: true, notify: true,
}, },
/**
* Available languages.
*/
languagesToVoices: {
type: Array,
value: [],
notify: true,
},
},
/** @override */
ready: function() {
this.addWebUIListener(
'all-voice-data-updated', this.populateVoiceList_.bind(this));
chrome.send('getAllTtsVoiceData');
},
/**
* 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.
let result = {};
voices.forEach(voice => {
if (!result[voice.languageCode]) {
result[voice.languageCode] = {
language: voice.displayLanguage,
code: voice.languageCode,
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);
});
this.set('languagesToVoices', Object.values(result));
},
/**
* 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};
});
}, },
/** @private */ /** @private */
......
...@@ -48,6 +48,13 @@ Polymer({ ...@@ -48,6 +48,13 @@ Polymer({
value: false, value: false,
}, },
/** If this is a dictionary pref, this is the key for the item
we are interested in. */
prefKey: {
type: String,
value: null,
},
/** /**
* The value of the "custom" item. * The value of the "custom" item.
* @private * @private
...@@ -63,7 +70,7 @@ Polymer({ ...@@ -63,7 +70,7 @@ Polymer({
}, },
observers: [ observers: [
'updateSelected_(menuOptions, pref.value)', 'updateSelected_(menuOptions, pref.value.*, prefKey)',
], ],
/** /**
...@@ -76,10 +83,15 @@ Polymer({ ...@@ -76,10 +83,15 @@ Polymer({
if (selected == this.notFoundValue_) if (selected == this.notFoundValue_)
return; return;
const prefValue = if (this.prefKey) {
Settings.PrefUtil.stringToPrefValue(selected, assert(this.pref)); assert(this.pref);
if (prefValue !== undefined) this.set(`pref.value.${this.prefKey}`, selected);
this.set('pref.value', prefValue); } else {
const prefValue =
Settings.PrefUtil.stringToPrefValue(selected, assert(this.pref));
if (prefValue !== undefined)
this.set('pref.value', prefValue);
}
}, },
/** /**
...@@ -90,7 +102,7 @@ Polymer({ ...@@ -90,7 +102,7 @@ Polymer({
if (this.menuOptions === null || !this.menuOptions.length) if (this.menuOptions === null || !this.menuOptions.length)
return; return;
const prefValue = this.pref.value; const prefValue = this.prefStringValue_();
const option = this.menuOptions.find(function(menuItem) { const option = this.menuOptions.find(function(menuItem) {
return menuItem.value == prefValue; return menuItem.value == prefValue;
}); });
...@@ -98,12 +110,25 @@ Polymer({ ...@@ -98,12 +110,25 @@ Polymer({
// Wait for the dom-repeat to populate the <select> before setting // Wait for the dom-repeat to populate the <select> before setting
// <select>#value so the correct option gets selected. // <select>#value so the correct option gets selected.
this.async(() => { this.async(() => {
this.$.dropdownMenu.value = option == undefined ? this.$.dropdownMenu.value =
this.notFoundValue_ : option == undefined ? this.notFoundValue_ : prefValue;
Settings.PrefUtil.prefToString(assert(this.pref));
}); });
}, },
/**
* Gets the current value of the preference as a string.
* @return {string}
* @private
*/
prefStringValue_: function() {
if (this.prefKey) {
// Dictionary pref, values are always strings.
return this.pref.value[this.prefKey];
} else {
return Settings.PrefUtil.prefToString(assert(this.pref));
}
},
/** /**
* @param {?DropdownMenuOptionList} menuOptions * @param {?DropdownMenuOptionList} menuOptions
* @param {string} prefValue * @param {string} prefValue
...@@ -115,8 +140,8 @@ Polymer({ ...@@ -115,8 +140,8 @@ Polymer({
if (!menuOptions || !menuOptions.length) if (!menuOptions || !menuOptions.length)
return false; return false;
const option = menuOptions.find(function(menuItem) { const option = menuOptions.find((menuItem) => {
return menuItem.value == prefValue; return menuItem.value == this.prefStringValue_();
}); });
return !option; return !option;
}, },
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
namespace settings { namespace settings {
void TtsHandler::HandleGetGoogleTtsVoiceData(const base::ListValue* args) { void TtsHandler::HandleGetAllTtsVoiceData(const base::ListValue* args) {
OnVoicesChanged(); OnVoicesChanged();
} }
...@@ -25,27 +25,26 @@ void TtsHandler::OnVoicesChanged() { ...@@ -25,27 +25,26 @@ void TtsHandler::OnVoicesChanged() {
impl->GetVoices(Profile::FromWebUI(web_ui()), &voices); impl->GetVoices(Profile::FromWebUI(web_ui()), &voices);
base::ListValue responses; base::ListValue responses;
for (const auto& voice : voices) { for (const auto& voice : voices) {
if (voice.extension_id != extension_misc::kSpeechSynthesisExtensionId)
continue;
base::DictionaryValue response; base::DictionaryValue response;
std::string language_code = l10n_util::GetLanguage(voice.lang);
response.SetPath({"name"}, base::Value(voice.name)); response.SetPath({"name"}, base::Value(voice.name));
response.SetPath({"languageCode"}, base::Value(language_code));
response.SetPath({"extensionId"}, base::Value(voice.extension_id));
response.SetPath( response.SetPath(
{"language"}, {"displayLanguage"},
base::Value(l10n_util::GetDisplayNameForLocale( base::Value(l10n_util::GetDisplayNameForLocale(
voice.lang, g_browser_process->GetApplicationLocale(), true))); language_code, g_browser_process->GetApplicationLocale(), true)));
response.SetPath({"builtIn"}, base::Value(true));
responses.GetList().push_back(std::move(response)); responses.GetList().push_back(std::move(response));
} }
AllowJavascript(); AllowJavascript();
CallJavascriptFunction("cr.webUIListenerCallback", CallJavascriptFunction("cr.webUIListenerCallback",
base::Value("google-voice-data-updated"), responses); base::Value("all-voice-data-updated"), responses);
} }
void TtsHandler::RegisterMessages() { void TtsHandler::RegisterMessages() {
web_ui()->RegisterMessageCallback( web_ui()->RegisterMessageCallback(
"getGoogleTtsVoiceData", "getAllTtsVoiceData",
base::BindRepeating(&TtsHandler::HandleGetGoogleTtsVoiceData, base::BindRepeating(&TtsHandler::HandleGetAllTtsVoiceData,
base::Unretained(this))); base::Unretained(this)));
} }
......
...@@ -16,7 +16,7 @@ class TtsHandler : public SettingsPageUIHandler, public VoicesChangedDelegate { ...@@ -16,7 +16,7 @@ class TtsHandler : public SettingsPageUIHandler, public VoicesChangedDelegate {
TtsHandler() = default; TtsHandler() = default;
~TtsHandler() override = default; ~TtsHandler() override = default;
void HandleGetGoogleTtsVoiceData(const base::ListValue* args); void HandleGetAllTtsVoiceData(const base::ListValue* args);
// SettingsPageUIHandler implementation. // SettingsPageUIHandler implementation.
void RegisterMessages() override; void RegisterMessages() override;
......
...@@ -158,5 +158,40 @@ cr.define('settings_dropdown_menu', function() { ...@@ -158,5 +158,40 @@ cr.define('settings_dropdown_menu', function() {
assertTrue(customOption.disabled); assertTrue(customOption.disabled);
}); });
}); });
test('works with dictionary pref', function testDictionaryPref() {
dropdown.pref = {
key: 'test.dictionary',
type: chrome.settingsPrivate.PrefType.DICTIONARY,
value: {
'key1': 'value1',
'key2': 'value2',
},
};
dropdown.prefKey = 'key2';
dropdown.menuOptions = [
{value: 'value2', name: 'Option 2'},
{value: 'value3', name: 'Option 3'},
{value: 'value4', name: 'Option 4'},
];
return waitUntilDropdownUpdated().then(function() {
// Initially selected item.
assertEquals(
'Option 2',
selectElement.selectedOptions[0].textContent.trim());
// Selecting an item updates the pref.
return simulateChangeEvent('value3');
}).then(function() {
assertEquals('value3', dropdown.pref.value['key2']);
// Updating the pref selects an item.
dropdown.set('pref.value.key2', 'value4');
return waitUntilDropdownUpdated();
}).then(function() {
assertEquals('value4', selectElement.value);
});
});
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment