blob: 43787f93abb427cf73164dadbd6ca6bc0958a18c [file] [log] [blame]
// Copyright 2015 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 'settings-languages-page' is the settings page
* for language and input method settings.
*/
cr.exportPath('settings');
/**
* @type {number} Millisecond delay that can be used when closing an action
* menu to keep it briefly on-screen.
*/
settings.kMenuCloseDelay = 100;
/**
* Name of the language setting is shown uma histogram.
* @type {string}
*/
const LANGUAGE_SETTING_IS_SHOWN_UMA_NAME = 'Translate.LanguageSettingsIsShown';
(function() {
'use strict';
Polymer({
is: 'settings-languages-page',
properties: {
/**
* Preferences state.
*/
prefs: {
type: Object,
notify: true,
},
/**
* Read-only reference to the languages model provided by the
* 'settings-languages' instance.
* @type {!LanguagesModel|undefined}
*/
languages: {
type: Object,
notify: true,
},
/** @type {!LanguageHelper} */
languageHelper: Object,
// <if expr="not is_macosx">
/** @private */
spellCheckSecondaryText_: {
type: String,
value: '',
computed: 'getSpellCheckSecondaryText_(languages.enabled.*, ' +
'languages.forcedSpellCheckLanguages.*, ' +
'prefs.browser.enable_spellchecking.*)',
},
/** @private */
spellCheckLanguages_: {
type: Array,
value: function() {
return [];
},
},
/** @private */
spellCheckDisabled_: {
type: Boolean,
value: false,
},
// </if>
/**
* The language to display the details for.
* @type {!LanguageState|undefined}
* @private
*/
detailLanguage_: Object,
/**
* Whether the language settings list is opened.
* @private
*/
languagesOpened_: {
type: Boolean,
observer: 'onLanguagesOpenedChanged_',
},
/** @private */
showAddLanguagesDialog_: Boolean,
/** @private {!Map<string, string>} */
focusConfig_: {
type: Object,
value: function() {
const map = new Map();
// <if expr="not is_macosx">
if (settings.routes.EDIT_DICTIONARY) {
map.set(
settings.routes.EDIT_DICTIONARY.path,
'#spellCheckCollapse .subpage-arrow');
}
// </if>
// <if expr="chromeos">
if (settings.routes.INPUT_METHODS) {
map.set(
settings.routes.INPUT_METHODS.path,
'#inputMethodsCollapse .subpage-arrow');
}
// </if>
return map;
},
},
},
observers: [
'updateSpellcheckLanguages_(languages.enabled.*, ' +
'languages.forcedSpellCheckLanguages.*)',
'updateSpellcheckEnabled_(prefs.browser.enable_spellchecking.*)',
],
/**
* Stamps and opens the Add Languages dialog, registering a listener to
* disable the dialog's dom-if again on close.
* @param {!Event} e
* @private
*/
onAddLanguagesTap_: function(e) {
e.preventDefault();
this.showAddLanguagesDialog_ = true;
this.async(function() {
const dialog = this.$$('settings-add-languages-dialog');
dialog.addEventListener('close', () => {
this.showAddLanguagesDialog_ = false;
cr.ui.focusWithoutInk(assert(this.$.addLanguages));
});
});
},
/**
* Used to determine which "Move" buttons to show for ordering enabled
* languages.
* @param {number} n
* @param {!LanguageState} language
* @return {boolean} True if |language| is at the |n|th index in the list of
* enabled languages.
* @private
*/
isNthLanguage_: function(n, language) {
const compareLanguage = assert(this.languages.enabled[n]);
return language.language == compareLanguage.language;
},
/**
* @param {!LanguageState} language
* @return {boolean} True if the "Move to top" option for |language| should be
* visible.
* @private
*/
showMoveUp_: function(language) {
// "Move up" is a no-op for the top language, and redundant with
// "Move to top" for the 2nd language.
return !this.isNthLanguage_(0, language) &&
!this.isNthLanguage_(1, language);
},
/**
* @param {!LanguageState} language
* @return {boolean} True if the "Move down" option for |language| should be
* visible.
* @private
*/
showMoveDown_: function(language) {
return !this.isNthLanguage_(this.languages.enabled.length - 1, language);
},
/**
* @param {!Object} change Polymer change object for languages.enabled.*.
* @return {boolean} True if there are less than 2 languages.
*/
isHelpTextHidden_: function(change) {
return this.languages.enabled.length <= 1;
},
// <if expr="chromeos">
/**
* Applies Chrome OS session tweaks to the menu.
* @param {!CrActionMenuElement} menu
* @private
*/
tweakMenuForCrOS_: function(menu) {
// In a CrOS multi-user session, the primary user controls the UI language.
// TODO(michaelpg): The language selection should not be hidden, but should
// show a policy indicator. crbug.com/648498
if (this.isSecondaryUser_())
menu.querySelector('#uiLanguageItem').hidden = true;
// The UI language choice doesn't persist for guests.
if (loadTimeData.getBoolean('isGuest'))
menu.querySelector('#uiLanguageItem').hidden = true;
},
/**
* Opens the Manage Input Methods page.
* @private
*/
onManageInputMethodsTap_: function() {
settings.navigateTo(settings.routes.INPUT_METHODS);
},
/**
* Handler for tap and <Enter> events on an input method on the main page,
* which sets it as the current input method.
* @param {!{model: !{item: !chrome.languageSettingsPrivate.InputMethod},
* target: !{tagName: string},
* type: string,
* key: (string|undefined)}} e
*/
onInputMethodTap_: function(e) {
// Taps on the button are handled in onInputMethodOptionsTap_.
// TODO(dschuyler): The row has two operations that are not clearly
// delineated. crbug.com/740691
if (e.target.tagName == 'BUTTON')
return;
// Ignore key presses other than <Enter>.
if (e.type == 'keypress' && e.key != 'Enter')
return;
// Set the input method.
this.languageHelper.setCurrentInputMethod(e.model.item.id);
},
/**
* Opens the input method extension's options page in a new tab (or focuses
* an existing instance of the IME's options).
* @param {!{model: !{item: chrome.languageSettingsPrivate.InputMethod}}} e
* @private
*/
onInputMethodOptionsTap_: function(e) {
this.languageHelper.openInputMethodOptions(e.model.item.id);
},
// </if>
// <if expr="chromeos or is_win">
/**
* @return {boolean} True for a secondary user in a multi-profile session.
* @private
*/
isSecondaryUser_: function() {
return cr.isChromeOS && loadTimeData.getBoolean('isSecondaryUser');
},
/**
* @param {string} languageCode The language code identifying a language.
* @param {string} prospectiveUILanguage The prospective UI language.
* @return {boolean} True if the prospective UI language is set to
* |languageCode| but requires a restart to take effect.
* @private
*/
isRestartRequired_: function(languageCode, prospectiveUILanguage) {
return prospectiveUILanguage == languageCode &&
this.languageHelper.requiresRestart();
},
/**
* @param {!LanguageState} languageState
* @param {string} prospectiveUILanguage The chosen UI language.
* @return {boolean} True if the given language cannot be set as the
* prospective UI language by the user.
* @private
*/
disableUILanguageCheckbox_: function(languageState, prospectiveUILanguage) {
// UI language setting belongs to the primary user.
if (this.isSecondaryUser_())
return true;
// If the language cannot be a UI language, we can't set it as the
// prospective UI language.
if (!languageState.language.supportsUI)
return true;
// Unchecking the currently chosen language doesn't make much sense.
if (languageState.language.code == prospectiveUILanguage)
return true;
// Otherwise, the prospective language can be changed to this language.
return false;
},
/**
* Handler for changes to the UI language checkbox.
* @param {!{target: !PaperCheckboxElement}} e
* @private
*/
onUILanguageChange_: function(e) {
// We don't support unchecking this checkbox. TODO(michaelpg): Ask for a
// simpler widget.
assert(e.target.checked);
this.languageHelper.setProspectiveUILanguage(
this.detailLanguage_.language.code);
this.closeMenuSoon_();
},
// </if>
/**
* @param {!chrome.languageSettingsPrivate.Language} language
* @param {string} targetLanguageCode The default translate target language.
* @return {boolean} True if the translate checkbox should be disabled.
* @private
*/
disableTranslateCheckbox_: function(language, targetLanguageCode) {
if (!language.supportsTranslate)
return true;
return this.languageHelper.convertLanguageCodeForTranslate(language.code) ==
targetLanguageCode;
},
/**
* Handler for changes to the translate checkbox.
* @param {!{target: !PaperCheckboxElement}} e
* @private
*/
onTranslateCheckboxChange_: function(e) {
if (e.target.checked) {
this.languageHelper.enableTranslateLanguage(
this.detailLanguage_.language.code);
} else {
this.languageHelper.disableTranslateLanguage(
this.detailLanguage_.language.code);
}
this.closeMenuSoon_();
},
/**
* Returns "complex" if the menu includes checkboxes, which should change the
* spacing of items and show a separator in the menu.
* @param {boolean} translateEnabled
* @return {string}
*/
getMenuClass_: function(translateEnabled) {
if (translateEnabled || cr.isChromeOS || cr.isWindows)
return 'complex';
return '';
},
/**
* Moves the language to the top of the list.
* @private
*/
onMoveToTopTap_: function() {
/** @type {!CrActionMenuElement} */ (this.$.menu.get()).close();
this.languageHelper.moveLanguageToFront(this.detailLanguage_.language.code);
},
/**
* Moves the language up in the list.
* @private
*/
onMoveUpTap_: function() {
/** @type {!CrActionMenuElement} */ (this.$.menu.get()).close();
this.languageHelper.moveLanguage(
this.detailLanguage_.language.code, true /* upDirection */);
},
/**
* Moves the language down in the list.
* @private
*/
onMoveDownTap_: function() {
/** @type {!CrActionMenuElement} */ (this.$.menu.get()).close();
this.languageHelper.moveLanguage(
this.detailLanguage_.language.code, false /* upDirection */);
},
/**
* Disables the language.
* @private
*/
onRemoveLanguageTap_: function() {
/** @type {!CrActionMenuElement} */ (this.$.menu.get()).close();
this.languageHelper.disableLanguage(this.detailLanguage_.language.code);
},
// <if expr="chromeos or is_win">
/**
* Checks whether the prospective UI language (the pref that indicates what
* language to use in Chrome) matches the current language. This pref is used
* only on Chrome OS and Windows; we don't control the UI language elsewhere.
* @param {string} languageCode The language code identifying a language.
* @param {string} prospectiveUILanguage The prospective UI language.
* @return {boolean} True if the given language matches the prospective UI
* pref (which may be different from the actual UI language).
* @private
*/
isProspectiveUILanguage_: function(languageCode, prospectiveUILanguage) {
return languageCode == prospectiveUILanguage;
},
/**
* @param {string} prospectiveUILanguage
* @return {string}
* @private
*/
getProspectiveUILanguageName_: function(prospectiveUILanguage) {
return this.languageHelper.getLanguage(prospectiveUILanguage).displayName;
},
// </if>
/**
* @return {string}
* @private
*/
getLanguageListTwoLine_: function() {
return cr.isChromeOS || cr.isWindows ? 'two-line' : '';
},
// <if expr="not is_macosx">
/**
* Returns the secondary text for the spell check subsection based on the
* enabled spell check languages, listing at most 2 languages.
* @return {string}
* @private
*/
getSpellCheckSecondaryText_: function() {
if (this.getSpellCheckDisabled_())
return loadTimeData.getString('spellCheckDisabled');
const enabledSpellCheckLanguages =
this.getSpellCheckLanguages_().filter(function(languageState) {
return (languageState.spellCheckEnabled || languageState.isManaged) &&
languageState.language.supportsSpellcheck;
});
switch (enabledSpellCheckLanguages.length) {
case 0:
return '';
case 1:
return enabledSpellCheckLanguages[0].language.displayName;
case 2:
return loadTimeData.getStringF(
'spellCheckSummaryTwoLanguages',
enabledSpellCheckLanguages[0].language.displayName,
enabledSpellCheckLanguages[1].language.displayName);
case 3:
// "foo, bar, and 1 other"
return loadTimeData.getStringF(
'spellCheckSummaryThreeLanguages',
enabledSpellCheckLanguages[0].language.displayName,
enabledSpellCheckLanguages[1].language.displayName);
default:
// "foo, bar, and [N-2] others"
return loadTimeData.getStringF(
'spellCheckSummaryMultipleLanguages',
enabledSpellCheckLanguages[0].language.displayName,
enabledSpellCheckLanguages[1].language.displayName,
(enabledSpellCheckLanguages.length - 2).toLocaleString());
}
},
/**
* Returns whether spellcheck is disabled by policy or not.
* @return {boolean}
* @private
*/
getSpellCheckDisabled_: function() {
const pref = /** @type {!chrome.settingsPrivate.PrefObject} */ (
this.get('browser.enable_spellchecking', this.prefs));
return pref.value === false;
},
/**
* Returns an array of enabled languages, plus spellcheck languages that are
* forced by policy.
* @return {!Array<!LanguageState|!ForcedLanguageState>}
* @private
*/
getSpellCheckLanguages_: function() {
return this.languages.enabled.concat(
this.languages.forcedSpellCheckLanguages.map(
language => ({'language': language, isManaged: true})));
},
/** @private */
updateSpellcheckLanguages_: function() {
this.set('spellCheckLanguages_', this.getSpellCheckLanguages_());
// Notify Polymer of subproperties that might have changed on the items in
// the spellCheckLanguages_ array, to make sure the UI updates. Polymer
// would otherwise not notice the changes in the subproperties, as some of
// them are references to those from |this.languages.enabled|. It would be
// possible to |this.linkPaths()| objects from |this.languages.enabled| to
// |this.spellCheckLanguages_|, but that would require complex housekeeping
// to |this.unlinkPaths()| as |this.languages.enabled| changes.
for (let i = 0; i < this.spellCheckLanguages_.length; i++) {
this.notifyPath(`spellCheckLanguages_.${i}.isManaged`);
this.notifyPath(`spellCheckLanguages_.${i}.spellCheckEnabled`);
}
},
/** @private */
updateSpellcheckEnabled_: function() {
this.set('spellCheckDisabled_', this.getSpellCheckDisabled_());
// If the spellcheck section was expanded, close it.
if (this.spellCheckDisabled_)
this.set('spellCheckOpened_', false);
},
/**
* Opens the Custom Dictionary page.
* @private
*/
onEditDictionaryTap_: function() {
settings.navigateTo(settings.routes.EDIT_DICTIONARY);
},
/**
* Handler for enabling or disabling spell check.
* @param {!{target: Element, model: !{item: !LanguageState}}} e
*/
onSpellCheckChange_: function(e) {
const item = e.model.item;
if (!item.language.supportsSpellcheck)
return;
this.languageHelper.toggleSpellCheck(
item.language.code, !item.spellCheckEnabled);
},
/**
* @return {string}
* @private
*/
getSpellCheckListTwoLine_: function() {
return this.spellCheckSecondaryText_.length ? 'two-line' : '';
},
// </if>
/**
* Returns either the "selected" class, if the language matches the
* prospective UI language, or an empty string. Languages can only be
* selected on Chrome OS and Windows.
* @param {string} languageCode The language code identifying a language.
* @param {string} prospectiveUILanguage The prospective UI language.
* @return {string} The class name for the language item.
* @private
*/
getLanguageItemClass_: function(languageCode, prospectiveUILanguage) {
if ((cr.isChromeOS || cr.isWindows) &&
languageCode == prospectiveUILanguage) {
return 'selected';
}
return '';
},
// <if expr="chromeos">
/**
* @param {string} id The input method ID.
* @param {string} currentId The ID of the currently enabled input method.
* @return {boolean} True if the IDs match.
* @private
*/
isCurrentInputMethod_: function(id, currentId) {
assert(cr.isChromeOS);
return id == currentId;
},
/**
* @param {string} id The input method ID.
* @param {string} currentId The ID of the currently enabled input method.
* @return {string} The class for the input method item.
* @private
*/
getInputMethodItemClass_: function(id, currentId) {
assert(cr.isChromeOS);
return this.isCurrentInputMethod_(id, currentId) ? 'selected' : '';
},
getInputMethodName_: function(id) {
assert(cr.isChromeOS);
const inputMethod =
this.languages.inputMethods.enabled.find(function(inputMethod) {
return inputMethod.id == id;
});
return inputMethod ? inputMethod.displayName : '';
},
// </if>
/**
* @param {!Event} e
* @private
*/
onDotsTap_: function(e) {
// Set a copy of the LanguageState object since it is not data-bound to the
// languages model directly.
this.detailLanguage_ = /** @type {!LanguageState} */ (Object.assign(
{},
/** @type {!{model: !{item: !LanguageState}}} */ (e).model.item));
// Ensure the template has been stamped.
let menu = /** @type {?CrActionMenuElement} */ (this.$.menu.getIfExists());
if (!menu) {
menu = /** @type {!CrActionMenuElement} */ (this.$.menu.get());
// <if expr="chromeos">
this.tweakMenuForCrOS_(menu);
// </if>
}
menu.showAt(/** @type {!Element} */ (e.target));
},
/**
* @param {boolean} newVal The new value of languagesOpened_.
* @param {boolean} oldVal The old value of languagesOpened_.
* @private
*/
onLanguagesOpenedChanged_: function(newVal, oldVal) {
if (!oldVal && newVal) {
chrome.send(
'metricsHandler:recordBooleanHistogram',
[LANGUAGE_SETTING_IS_SHOWN_UMA_NAME, true]);
}
},
/**
* Closes the shared action menu after a short delay, so when a checkbox is
* tapped it can be seen to change state before disappearing.
* @private
*/
closeMenuSoon_: function() {
const menu = /** @type {!CrActionMenuElement} */ (this.$.menu.get());
setTimeout(function() {
if (menu.open)
menu.close();
}, settings.kMenuCloseDelay);
},
// <if expr="chromeos or is_win">
/**
* Handler for the restart button.
* @private
*/
onRestartTap_: function() {
// <if expr="chromeos">
settings.LifetimeBrowserProxyImpl.getInstance().signOutAndRestart();
// </if>
// <if expr="is_win">
settings.LifetimeBrowserProxyImpl.getInstance().restart();
// </if>
},
// </if>
/**
* Toggles the expand button within the element being listened to.
* @param {!Event} e
* @private
*/
toggleExpandButton_: function(e) {
// The expand button handles toggling itself.
const expandButtonTag = 'CR-EXPAND-BUTTON';
if (e.target.tagName == expandButtonTag)
return;
if (!e.currentTarget.hasAttribute('actionable'))
return;
/** @type {!CrExpandButtonElement} */
const expandButton = e.currentTarget.querySelector(expandButtonTag);
assert(expandButton);
expandButton.expanded = !expandButton.expanded;
},
});
})();