chromium/chrome/browser/resources/settings/languages_page/languages_page.ts

// Copyright 2015 The Chromium Authors
// 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.js';
import 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import 'chrome://resources/cr_elements/icons.html.js';
import 'chrome://resources/cr_elements/cr_shared_style.css.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/js/action_link.js';
import 'chrome://resources/cr_elements/action_link.css.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 '../icons.html.js';
import '../relaunch_confirmation_dialog.js';
import '../settings_shared.css.js';
import '../settings_vars.css.js';

import type {CrActionMenuElement} from '//resources/cr_elements/cr_action_menu/cr_action_menu.js';
import type {CrCheckboxElement} from 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.js';
import type {CrLazyRenderElement} from 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import {assert} from 'chrome://resources/js/assert.js';
import {isWindows} from 'chrome://resources/js/platform.js';
import {focusWithoutInk} from 'chrome://resources/js/focus_without_ink.js';
import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import type {DomRepeatEvent} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import { 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 {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js';

import {RelaunchMixin, RestartType} from '../relaunch_mixin.js';
import {routes} from '../route.js';
import type {Route} from '../router.js';
import { RouteObserverMixin} from '../router.js';

import {getTemplate} from './languages_page.html.js';
import type { LanguageSettingsMetricsProxy} from './languages_settings_metrics_proxy.js';
import {LanguageSettingsActionType, LanguageSettingsMetricsProxyImpl, LanguageSettingsPageImpressionType} from './languages_settings_metrics_proxy.js';
import type {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))));

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,
      },
    };
  }

  languages?: LanguagesModel;
  languageHelper: LanguageHelper;
  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 onAddLanguagesClick_(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);
  }

  /**
   * Formats language index (zero-indexed)
   */
  private formatIndex_(index: number): string {
    return (index + 1).toLocaleString();
  }

  /**
   * 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 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';
    }
  }

  // <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<HTMLElement>('#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 onRestartClick_() {
    this.performRestart(RestartType.RESTART);
  }
  // </if>

  /**
   * Moves the language to the top of the list.
   */
  private onMoveToTopClick_() {
    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 onMoveUpClick_() {
    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 onMoveDownClick_() {
    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 onRemoveLanguageClick_() {
    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 onDotsClick_(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);