blob: 03f38bfffbacd98677719cb9942bf3167a0d45ec [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.
*/
// clang-format off
import 'chrome://resources/cr_components/managed_dialog/managed_dialog.js';
import 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.m.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.m.js';
import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import 'chrome://resources/cr_elements/icons.m.js';
import 'chrome://resources/cr_elements/shared_style_css.m.js';
import 'chrome://resources/cr_elements/shared_vars_css.m.js';
import 'chrome://resources/js/action_link.js';
import 'chrome://resources/cr_elements/action_link_css.m.js';
import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import './add_languages_dialog.js';
import '../controls/settings_toggle_button.js';
import '../icons.html.js';
import '../relaunch_confirmation_dialog.js';
import '../settings_shared.css.js';
import '../settings_vars.css.js';
import {CrActionMenuElement} from '//resources/cr_elements/cr_action_menu/cr_action_menu.js';
import {CrCheckboxElement} from 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.m.js';
import {CrLazyRenderElement} from 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
import {isWindows} from 'chrome://resources/js/cr.m.js';
import {focusWithoutInk} from 'chrome://resources/js/cr/ui/focus_without_ink.m.js';
import {I18nMixin, I18nMixinInterface} from 'chrome://resources/js/i18n_mixin.js';
import {DomRepeatEvent, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
// <if expr="is_win">
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
// </if>
import {SettingsToggleButtonElement} from '../controls/settings_toggle_button.js';
import {loadTimeData} from '../i18n_setup.js';
import {PrefsMixin, PrefsMixinInterface} from '../prefs/prefs_mixin.js';
import {RelaunchMixin, RelaunchMixinInterface, RestartType} from '../relaunch_mixin.js';
import {routes} from '../route.js';
import {Route, RouteObserverMixin, RouteObserverMixinInterface} from '../router.js';
import {getTemplate} from './languages_page.html.js';
import {LanguageSettingsActionType, LanguageSettingsMetricsProxy, LanguageSettingsMetricsProxyImpl, LanguageSettingsPageImpressionType} from './languages_settings_metrics_proxy.js';
import {LanguageHelper, LanguagesModel, LanguageState} from './languages_types.js';
// clang-format on
/**
* Millisecond delay that can be used when closing an action menu to keep it
* briefly on-screen.
*/
export const kMenuCloseDelay: number = 100;
export interface SettingsLanguagesPageElement {
$: {
menu: CrLazyRenderElement<CrActionMenuElement>,
};
}
const SettingsLanguagesPageElementBase =
RouteObserverMixin(RelaunchMixin(I18nMixin(PrefsMixin(PolymerElement)))) as {
new (): PolymerElement & RelaunchMixinInterface & I18nMixinInterface & PrefsMixinInterface & RouteObserverMixinInterface,
};
export class SettingsLanguagesPageElement extends
SettingsLanguagesPageElementBase {
static get is() {
return 'settings-languages-page';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
/**
* Preferences state.
*/
prefs: {
type: Object,
notify: true,
},
/**
* Read-only reference to the languages model provided by the
* 'settings-languages' instance.
*/
languages: {
type: Object,
notify: true,
},
languageHelper: Object,
/**
* The language to display the details for.
*/
detailLanguage_: Object,
showAddLanguagesDialog_: Boolean,
addLanguagesDialogLanguages_: Array,
showManagedLanguageDialog_: {
type: Boolean,
value: false,
},
enableDesktopDetailedLanguageSettings_: {
type: Boolean,
value: function() {
return loadTimeData.getBoolean(
'enableDesktopDetailedLanguageSettings');
},
},
};
}
languages?: LanguagesModel;
languageHelper: LanguageHelper;
private enableDesktopDetailedLanguageSettings_: boolean;
private detailLanguage_?: LanguageState;
private showAddLanguagesDialog_: boolean;
private addLanguagesDialogLanguages_:
chrome.languageSettingsPrivate.Language[]|null;
private showManagedLanguageDialog_: boolean;
private languageSettingsMetricsProxy_: LanguageSettingsMetricsProxy =
LanguageSettingsMetricsProxyImpl.getInstance();
// <if expr="is_win">
private isChangeInProgress_: boolean = false;
// </if>
/**
* Stamps and opens the Add Languages dialog, registering a listener to
* disable the dialog's dom-if again on close.
*/
private onAddLanguagesTap_(e: Event) {
e.preventDefault();
this.languageSettingsMetricsProxy_.recordPageImpressionMetric(
LanguageSettingsPageImpressionType.ADD_LANGUAGE);
this.addLanguagesDialogLanguages_ = this.languages!.supported.filter(
language => this.languageHelper.canEnableLanguage(language));
this.showAddLanguagesDialog_ = true;
}
private onLanguagesAdded_(e: CustomEvent<string[]>) {
const languagesToAdd = e.detail;
languagesToAdd.forEach(languageCode => {
this.languageHelper.enableLanguage(languageCode);
LanguageSettingsMetricsProxyImpl.getInstance().recordSettingsMetric(
LanguageSettingsActionType.LANGUAGE_ADDED);
});
}
private onAddLanguagesDialogClose_() {
this.showAddLanguagesDialog_ = false;
this.addLanguagesDialogLanguages_ = null;
const toFocus =
this.shadowRoot!.querySelector<HTMLElement>('#addLanguages');
assert(toFocus);
focusWithoutInk(toFocus);
}
/**
* Checks if there are supported languages that are not enabled but can be
* enabled.
* @return True if there is at least one available language.
*/
private canEnableSomeSupportedLanguage_(languages?: LanguagesModel): boolean {
return languages === undefined || languages.supported.some(language => {
return this.languageHelper.canEnableLanguage(language);
});
}
/**
* Used to determine whether to show the separator between checkbox settings
* and move buttons in the dialog menu.
* @return True if there is currently more than one selected language.
*/
private shouldShowDialogSeparator_(): boolean {
return this.languages !== undefined && this.languages.enabled.length > 1;
}
/**
* Used to determine which "Move" buttons to show for ordering enabled
* languages.
* @return True if |language| is at the |n|th index in the list of enabled
* languages.
*/
private isNthLanguage_(n: number): boolean {
if (this.languages === undefined || this.detailLanguage_ === undefined) {
return false;
}
if (n >= this.languages.enabled.length) {
return false;
}
const compareLanguage = this.languages.enabled[n]!;
return this.detailLanguage_.language === compareLanguage.language;
}
/**
* @return True if the "Move to top" option for |language| should be visible.
*/
private showMoveUp_(): boolean {
// "Move up" is a no-op for the top language, and redundant with
// "Move to top" for the 2nd language.
return !this.isNthLanguage_(0) && !this.isNthLanguage_(1);
}
/**
* @return True if the "Move down" option for |language| should be visible.
*/
private showMoveDown_(): boolean {
return this.languages !== undefined &&
!this.isNthLanguage_(this.languages.enabled.length - 1);
}
/**
* @param languageCode The language code identifying a language.
* @param translateTarget The target language.
* @return 'target' if |languageCode| matches the target language,
* 'non-target' otherwise.
*/
private isTranslationTarget_(languageCode: string, translateTarget: string):
string {
if (this.languageHelper.convertLanguageCodeForTranslate(languageCode) ===
translateTarget) {
return 'target';
} else {
return 'non-target';
}
}
private onTranslateToggleChange_(e: Event) {
this.languageSettingsMetricsProxy_.recordSettingsMetric(
(e.target as SettingsToggleButtonElement).checked ?
LanguageSettingsActionType.ENABLE_TRANSLATE_GLOBALLY :
LanguageSettingsActionType.DISABLE_TRANSLATE_GLOBALLY);
}
// <if expr="is_win">
/**
* @param languageCode The language code identifying a language.
* @param prospectiveUILanguage The prospective UI language.
* @return True if the prospective UI language is set to
* |languageCode| but requires a restart to take effect.
*/
private isRestartRequired_(
languageCode: string, prospectiveUILanguage: string): boolean {
return prospectiveUILanguage === languageCode &&
this.languageHelper.requiresRestart();
}
private onCloseMenu_() {
if (!this.isChangeInProgress_) {
return;
}
flush();
this.isChangeInProgress_ = false;
const restartButton = this.shadowRoot!.querySelector('#restartButton');
if (!restartButton) {
return;
}
focusWithoutInk(restartButton);
}
/**
* @param prospectiveUILanguage The chosen UI language.
* @return True if the given language cannot be set as the
* prospective UI language by the user.
*/
private disableUILanguageCheckbox_(
languageState: LanguageState, prospectiveUILanguage: string): boolean {
if (this.detailLanguage_ === undefined) {
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;
}
// Check if the language is prohibited by the current "AllowedLanguages"
// policy.
if (languageState.language.isProhibitedLanguage) {
return true;
}
// Otherwise, the prospective language can be changed to this language.
return false;
}
/**
* Handler for changes to the UI language checkbox.
*/
private onUILanguageChange_(e: Event) {
// We don't support unchecking this checkbox. TODO(michaelpg): Ask for a
// simpler widget.
assert((e.target as CrCheckboxElement).checked);
this.isChangeInProgress_ = true;
this.languageHelper.setProspectiveUILanguage(
this.detailLanguage_!.language.code);
this.languageHelper.moveLanguageToFront(
this.detailLanguage_!.language.code);
LanguageSettingsMetricsProxyImpl.getInstance().recordSettingsMetric(
LanguageSettingsActionType.CHANGE_CHROME_LANGUAGE);
this.closeMenuSoon_();
}
/**
* 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 languageCode The language code identifying a language.
* @param prospectiveUILanguage The prospective UI language.
* @return True if the given language matches the prospective UI pref (which
* may be different from the actual UI language).
*/
private isProspectiveUILanguage_(
languageCode: string, prospectiveUILanguage: string): boolean {
return languageCode === prospectiveUILanguage;
}
/**
* Handler for the restart button.
*/
private onRestartTap_() {
this.performRestart(RestartType.RESTART);
}
// </if>
/**
* @param targetLanguageCode The default translate target language.
* @return True if the translate checkbox should be disabled.
*/
private disableTranslateCheckbox_(
languageState: LanguageState|undefined,
targetLanguageCode: string): boolean {
if (languageState === undefined || languageState.language === undefined ||
!languageState.language.supportsTranslate) {
return true;
}
if (this.languageHelper.isOnlyTranslateBlockedLanguage(languageState)) {
return true;
}
return this.languageHelper.convertLanguageCodeForTranslate(
languageState.language.code) === targetLanguageCode;
}
/**
* Handler for changes to the translate checkbox.
*/
private onTranslateCheckboxChange_(e: Event) {
if ((e.target as CrCheckboxElement).checked) {
this.languageHelper.enableTranslateLanguage(
this.detailLanguage_!.language.code);
this.languageSettingsMetricsProxy_.recordSettingsMetric(
LanguageSettingsActionType.ENABLE_TRANSLATE_FOR_SINGLE_LANGUAGE);
} else {
this.languageHelper.disableTranslateLanguage(
this.detailLanguage_!.language.code);
this.languageSettingsMetricsProxy_.recordSettingsMetric(
LanguageSettingsActionType.DISABLE_TRANSLATE_FOR_SINGLE_LANGUAGE);
}
this.closeMenuSoon_();
}
/**
* Returns "complex" if the menu includes checkboxes, which should change
* the spacing of items and show a separator in the menu.
*/
private getMenuClass_(translateEnabled: boolean): string {
if (isWindows ||
(translateEnabled && !this.enableDesktopDetailedLanguageSettings_)) {
return 'complex';
}
return '';
}
/**
* Moves the language to the top of the list.
*/
private onMoveToTopTap_() {
this.$.menu.get().close();
if (this.detailLanguage_!.isForced) {
// If language is managed, show dialog to inform user it can't be modified
this.showManagedLanguageDialog_ = true;
return;
}
this.languageHelper.moveLanguageToFront(
this.detailLanguage_!.language.code);
this.languageSettingsMetricsProxy_.recordSettingsMetric(
LanguageSettingsActionType.LANGUAGE_LIST_REORDERED);
}
/**
* Moves the language up in the list.
*/
private onMoveUpTap_() {
this.$.menu.get().close();
if (this.detailLanguage_!.isForced) {
// If language is managed, show dialog to inform user it can't be modified
this.showManagedLanguageDialog_ = true;
return;
}
this.languageHelper.moveLanguage(
this.detailLanguage_!.language.code, true /* upDirection */);
this.languageSettingsMetricsProxy_.recordSettingsMetric(
LanguageSettingsActionType.LANGUAGE_LIST_REORDERED);
}
/**
* Moves the language down in the list.
*/
private onMoveDownTap_() {
this.$.menu.get().close();
if (this.detailLanguage_!.isForced) {
// If language is managed, show dialog to inform user it can't be modified
this.showManagedLanguageDialog_ = true;
return;
}
this.languageHelper.moveLanguage(
this.detailLanguage_!.language.code, false /* upDirection */);
this.languageSettingsMetricsProxy_.recordSettingsMetric(
LanguageSettingsActionType.LANGUAGE_LIST_REORDERED);
}
/**
* Disables the language.
*/
private onRemoveLanguageTap_() {
this.$.menu.get().close();
if (this.detailLanguage_!.isForced) {
// If language is managed, show dialog to inform user it can't be modified
this.showManagedLanguageDialog_ = true;
return;
}
this.languageHelper.disableLanguage(this.detailLanguage_!.language.code);
this.languageSettingsMetricsProxy_.recordSettingsMetric(
LanguageSettingsActionType.LANGUAGE_REMOVED);
}
/**
* 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 languageCode The language code identifying a language.
* @param prospectiveUILanguage The prospective UI language.
* @return The class name for the language item.
*/
private getLanguageItemClass_(
languageCode: string, prospectiveUILanguage: string): string {
if (isWindows && languageCode === prospectiveUILanguage) {
return 'selected';
}
return '';
}
private onDotsTap_(e: DomRepeatEvent<LanguageState>) {
// Set a copy of the LanguageState object since it is not data-bound to
// the languages model directly.
this.detailLanguage_ = Object.assign({}, e.model.item);
this.$.menu.get().showAt(e.target as HTMLElement);
this.languageSettingsMetricsProxy_.recordPageImpressionMetric(
LanguageSettingsPageImpressionType.LANGUAGE_OVERFLOW_MENU_OPENED);
}
/**
* Closes the shared action menu after a short delay, so when a checkbox is
* clicked it can be seen to change state before disappearing.
*/
private closeMenuSoon_() {
const menu = this.$.menu.get();
setTimeout(function() {
if (menu.open) {
menu.close();
}
}, kMenuCloseDelay);
}
/**
* Triggered when the managed language dialog is dismissed.
*/
private onManagedLanguageDialogClosed_() {
this.showManagedLanguageDialog_ = false;
}
override currentRouteChanged(currentRoute: Route) {
if (currentRoute === routes.LANGUAGES) {
this.languageSettingsMetricsProxy_.recordPageImpressionMetric(
LanguageSettingsPageImpressionType.MAIN);
}
}
}
declare global {
interface HTMLElementTagNameMap {
'settings-languages-page': SettingsLanguagesPageElement;
}
}
customElements.define(
SettingsLanguagesPageElement.is, SettingsLanguagesPageElement);