chromium/chrome/browser/resources/settings/appearance_page/appearance_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.

import 'chrome://resources/cr_components/managed_dialog/managed_dialog.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_link_row/cr_link_row.js';
import 'chrome://resources/cr_elements/cr_shared_style.css.js';
import 'chrome://resources/cr_elements/md_select.css.js';
import '../controls/controlled_radio_button.js';
import '/shared/settings/controls/extension_controlled_indicator.js';
import '../controls/settings_radio_group.js';
import '../controls/settings_toggle_button.js';
import '../settings_page/settings_animated_pages.js';
import '../settings_page/settings_subpage.js';
import '../settings_shared.css.js';
import '../settings_vars.css.js';
import './home_url_input.js';
import '../controls/settings_dropdown_menu.js';

import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js';
import {CustomizeColorSchemeModeBrowserProxy} from 'chrome://resources/cr_components/customize_color_scheme_mode/browser_proxy.js';
import type {CustomizeColorSchemeModeClientCallbackRouter, CustomizeColorSchemeModeHandlerInterface} from 'chrome://resources/cr_components/customize_color_scheme_mode/customize_color_scheme_mode.mojom-webui.js';
import {ColorSchemeMode} from 'chrome://resources/cr_components/customize_color_scheme_mode/customize_color_scheme_mode.mojom-webui.js';
import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {BaseMixin} from '../base_mixin.js';
import type {DropdownMenuOptionList, SettingsDropdownMenuElement} from '../controls/settings_dropdown_menu.js';
import type {SettingsToggleButtonElement} from '../controls/settings_toggle_button.js';
import {loadTimeData} from '../i18n_setup.js';
import type {AppearancePageVisibility} from '../page_visibility.js';
import {RelaunchMixin, RestartType} from '../relaunch_mixin.js';
import {routes} from '../route.js';
import {Router} from '../router.js';

import type {AppearanceBrowserProxy} from './appearance_browser_proxy.js';
import {AppearanceBrowserProxyImpl} from './appearance_browser_proxy.js';
import {getTemplate} from './appearance_page.html.js';

/**
 * This is the absolute difference maintained between standard and
 * fixed-width font sizes. http://crbug.com/91922.
 */
const SIZE_DIFFERENCE_FIXED_STANDARD: number = 3;

/**
 * ID for autogenerated themes. Should match
 * |ThemeService::kAutogeneratedThemeID|.
 */
const AUTOGENERATED_THEME_ID: string = 'autogenerated_theme_id';
/**
 * ID for user color themes. Should match
 * |ThemeService::kUserColorThemeID|.
 */
const USER_COLOR_THEME_ID: string = 'user_color_theme_id';

/**
 * 'settings-appearance-page' is the settings page containing appearance
 * settings.
 */

export interface SettingsAppearancePageElement {
  $: {
    colorSchemeModeRow: HTMLElement,
    colorSchemeModeSelect: HTMLSelectElement,
    defaultFontSize: SettingsDropdownMenuElement,
    showSavedTabGroups: SettingsToggleButtonElement,
    autoPinNewTabGroups: SettingsToggleButtonElement,
    zoomLevel: HTMLSelectElement,
    tabSearchPositionDropdown: SettingsDropdownMenuElement,
  };
}

// This must be kept in sync with the SystemTheme enum in
// ui/color/system_theme.h.
export enum SystemTheme {
  // Either classic or web theme.
  DEFAULT = 0,
  // <if expr="is_linux">
  GTK = 1,
  QT = 2,
  // </if>
}

const SettingsAppearancePageElementBase =
    RelaunchMixin(I18nMixin(PrefsMixin(BaseMixin(PolymerElement))));

export class SettingsAppearancePageElement extends
    SettingsAppearancePageElementBase {
  static get is() {
    return 'settings-appearance-page';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      /**
       * Dictionary defining page visibility.
       */
      pageVisibility: Object,

      prefs: {
        type: Object,
        notify: true,
      },

      defaultZoom_: Number,

      isWallpaperPolicyControlled_: {type: Boolean, value: true},

      colorSchemeModeOptions_: {
        readOnly: true,
        type: Array,
        value() {
          return [
            {
              value: ColorSchemeMode.kLight,
              name: loadTimeData.getString('lightMode'),
            },
            {
              value: ColorSchemeMode.kDark,
              name: loadTimeData.getString('darkMode'),
            },
            {
              value: ColorSchemeMode.kSystem,
              name: loadTimeData.getString('systemMode'),
            },
          ];
        },
      },

      selectedColorSchemeMode_: Number,

      /**
       * List of options for the font size drop-down menu.
       */
      fontSizeOptions_: {
        readOnly: true,
        type: Array,
        value() {
          return [
            {value: 9, name: loadTimeData.getString('verySmall')},
            {value: 12, name: loadTimeData.getString('small')},
            {value: 16, name: loadTimeData.getString('medium')},
            {value: 20, name: loadTimeData.getString('large')},
            {value: 24, name: loadTimeData.getString('veryLarge')},
          ];
        },
      },

      /**
       * Predefined zoom factors to be used when zooming in/out. These are in
       * ascending order. Values are displayed in the page zoom drop-down menu
       * as percentages.
       */
      pageZoomLevels_: Array,

      themeSublabel_: String,

      themeUrl_: String,

      systemTheme_: {
        type: Object,
        value: SystemTheme.DEFAULT,
      },

      focusConfig_: {
        type: Object,
        value() {
          const map = new Map();
          if (routes.FONTS) {
            map.set(routes.FONTS.path, '#customize-fonts-subpage-trigger');
          }
          return map;
        },
      },

      isForcedTheme_: {
        type: Boolean,
        computed: 'computeIsForcedTheme_(' +
            'prefs.autogenerated.theme.policy.color.controlledBy)',
      },

      // <if expr="is_linux">
      /**
       * Whether to show the "Custom Chrome Frame" setting.
       */
      showCustomChromeFrame_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('showCustomChromeFrame');
        },
      },
      // </if>

      showHoverCardImagesOption_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('showHoverCardImagesOption');
        },
      },

      showSavedTabGroupsInBookmarksBar_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('tabGroupsSaveUIUpdateEnabled');
        },
      },

      showAutoPinNewTabGroups_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('tabGroupsSaveUIUpdateEnabled');
        },
      },

      toolbarPinningEnabled_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('toolbarPinningEnabled');
        },
      },

      showManagedThemeDialog_: Boolean,

      sidePanelOptions_: {
        readOnly: true,
        type: Array,
        value() {
          return [
            {
              value: 'true',
              name: loadTimeData.getString('uiFeatureAlignRight'),
            },
            {
              value: 'false',
              name: loadTimeData.getString('uiFeatureAlignLeft'),
            },
          ];
        },
      },

      showTabSearchPositionSettings_: {
        type: Boolean,
        value() {
          return loadTimeData.getBoolean('showTabSearchPositionSettings');
        },
      },

      showTabSearchPositionRestartButton_: {
        type: Boolean,
        value: false,
      },

      showResetPinnedActionsButton_: {
        type: Boolean,
        value: false,
      },

      tabSearchOptions_: {
        readOnly: true,
        type: Array,
        value() {
          return [
            {
              value: 'true',
              name: loadTimeData.getString('uiFeatureAlignRight'),
            },
            {
              value: 'false',
              name: loadTimeData.getString('uiFeatureAlignLeft'),
            },
          ];
        },
      },
    };
  }

  static get observers() {
    return [
      'defaultFontSizeChanged_(prefs.webkit.webprefs.default_font_size.value)',
      'themeChanged_(' +
          'prefs.extensions.theme.id.value, systemTheme_, isForcedTheme_)',
      'updateShowTabSearchRestartButton_(' +
          'prefs.tab_search.is_right_aligned.value)',
      // <if expr="is_linux">
      'systemThemePrefChanged_(prefs.extensions.theme.system_theme.value)',
      // </if>
      'toolbarPinningStateChanged_(prefs.toolbar.pinned_actions.value,' +
          'prefs.browser.show_home_button.value,' +
          'prefs.browser.show_forward_button.value)',
    ];
  }

  pageVisibility: AppearancePageVisibility;
  private defaultZoom_: number;
  private isWallpaperPolicyControlled_: boolean;
  private fontSizeOptions_: DropdownMenuOptionList;
  private colorSchemeModeOptions_:
      Array<{value: ColorSchemeMode, name: string}>;
  private selectedColorSchemeMode_: ColorSchemeMode|undefined;
  private pageZoomLevels_: number[];
  private themeSublabel_: string;
  private themeUrl_: string;
  private systemTheme_: SystemTheme;
  private focusConfig_: Map<string, string>;
  private isForcedTheme_: boolean;
  private showHoverCardImagesOption_: boolean;
  private showSavedTabGroupsInBookmarksBar_: boolean;
  private showAutoPinNewTabGroups_: boolean;
  private showResetPinnedActionsButton_: boolean;
  private toolbarPinningEnabled_: boolean;

  // <if expr="is_linux">
  private showCustomChromeFrame_: boolean;
  // </if>

  private showTabSearchPositionSettings_: boolean;
  private showTabSearchPositionRestartButton_: boolean;
  private showManagedThemeDialog_: boolean;
  private sidePanelOptions_: DropdownMenuOptionList;
  private tabSearchOptions_: DropdownMenuOptionList;
  private appearanceBrowserProxy_: AppearanceBrowserProxy =
      AppearanceBrowserProxyImpl.getInstance();
  private colorSchemeModeHandler_: CustomizeColorSchemeModeHandlerInterface =
      CustomizeColorSchemeModeBrowserProxy.getInstance().handler;
  private colorSchemeModeCallbackRouter_:
      CustomizeColorSchemeModeClientCallbackRouter =
          CustomizeColorSchemeModeBrowserProxy.getInstance().callbackRouter;
  private setColorSchemeModeListenerId_: number|null = null;

  override ready() {
    super.ready();

    this.$.defaultFontSize.menuOptions = this.fontSizeOptions_;
    // TODO(dschuyler): Look into adding a listener for the
    // default zoom percent.
    this.appearanceBrowserProxy_.getDefaultZoom().then(zoom => {
      this.defaultZoom_ = zoom;
    });

    this.pageZoomLevels_ =
        JSON.parse(loadTimeData.getString('presetZoomFactors'));

    this.setColorSchemeModeListenerId_ =
        this.colorSchemeModeCallbackRouter_.setColorSchemeMode.addListener(
            (colorSchemeMode: ColorSchemeMode) => {
              this.selectedColorSchemeMode_ =
                  this.colorSchemeModeOptions_
                      .find(mode => colorSchemeMode === mode.value)
                      ?.value;
            });
    this.colorSchemeModeHandler_.initializeColorSchemeMode();
  }

  override disconnectedCallback() {
    super.disconnectedCallback();
    assert(this.setColorSchemeModeListenerId_);
    this.colorSchemeModeCallbackRouter_.removeListener(
        this.setColorSchemeModeListenerId_);
  }

  /** @return A zoom easier read by users. */
  private formatZoom_(zoom: number): number {
    return Math.round(zoom * 100);
  }

  /**
   * @param showHomepage Whether to show home page.
   * @param isNtp Whether to use the NTP as the home page.
   * @param homepageValue If not using NTP, use this URL.
   */
  private getShowHomeSubLabel_(
      showHomepage: boolean, isNtp: boolean, homepageValue: string): string {
    if (!showHomepage) {
      return this.i18n('homeButtonDisabled');
    }
    if (isNtp) {
      return this.i18n('homePageNtp');
    }
    return homepageValue || this.i18n('customWebAddress');
  }

  private onCustomizeFontsClick_() {
    Router.getInstance().navigateTo(routes.FONTS);
  }

  private onDisableExtension_() {
    this.dispatchEvent(new CustomEvent(
        'refresh-pref', {bubbles: true, composed: true, detail: 'homepage'}));
  }

  /**
   * @param value The changed font size slider value.
   */
  private defaultFontSizeChanged_(value: number) {
    // This pref is handled separately in some extensions, but here it is tied
    // to default_font_size (to simplify the UI).
    this.set(
        'prefs.webkit.webprefs.default_fixed_font_size.value',
        value - SIZE_DIFFERENCE_FIXED_STANDARD);
  }

  private onThemeClick_() {
    if (this.toolbarPinningEnabled_) {
      this.appearanceBrowserProxy_.openCustomizeChrome();
    } else {
      window.open(this.themeUrl_ || loadTimeData.getString('themesGalleryUrl'));
    }
  }

  private onCustomizeToolbarClick_() {
    this.appearanceBrowserProxy_.openCustomizeChromeToolbarSection();
  }

  private onUseDefaultClick_() {
    if (this.isForcedTheme_) {
      this.showManagedThemeDialog_ = true;
      return;
    }
    this.appearanceBrowserProxy_.useDefaultTheme();
  }

  private onResetPinnedToolbarActionsClick_() {
    this.appearanceBrowserProxy_.resetPinnedToolbarActions();
  }

  // <if expr="is_linux">
  private systemThemePrefChanged_(systemTheme: SystemTheme) {
    this.systemTheme_ = systemTheme;
  }

  /** @return Whether to show the "USE CLASSIC" button. */
  private showUseClassic_(themeId: string): boolean {
    return !!themeId || this.systemTheme_ !== SystemTheme.DEFAULT;
  }

  /** @return Whether to show the "USE GTK" button. */
  private showUseGtk_(themeId: string): boolean {
    return (!!themeId || this.systemTheme_ !== SystemTheme.GTK) &&
        !this.appearanceBrowserProxy_.isChildAccount();
  }

  /** @return Whether to show the "USE QT" button. */
  private showUseQt_(themeId: string): boolean {
    return (!!themeId || this.systemTheme_ !== SystemTheme.QT) &&
        !this.appearanceBrowserProxy_.isChildAccount();
  }

  /**
   * @return Whether to show the secondary area where "USE CLASSIC",
   *     "USE GTK", and "USE QT" buttons live.
   */
  private showThemesSecondary_(themeId: string): boolean {
    return !!themeId || !this.appearanceBrowserProxy_.isChildAccount();
  }

  private onUseGtkClick_() {
    if (this.isForcedTheme_) {
      this.showManagedThemeDialog_ = true;
      return;
    }
    this.appearanceBrowserProxy_.useGtkTheme();
  }

  private onUseQtClick_() {
    if (this.isForcedTheme_) {
      this.showManagedThemeDialog_ = true;
      return;
    }
    this.appearanceBrowserProxy_.useQtTheme();
  }

  /** @return Whether to show the color scheme mode toggle. */
  private showColorSchemeMode_(themeId: string): boolean {
    return !!themeId ||
        this.systemTheme_ !== SystemTheme.GTK &&
        this.systemTheme_ !== SystemTheme.QT;
  }
  // </if>

  private themeChanged_(themeId: string) {
    if (this.prefs === undefined || this.systemTheme_ === undefined) {
      return;
    }
    if (themeId.length > 0 && themeId !== AUTOGENERATED_THEME_ID &&
        themeId !== USER_COLOR_THEME_ID && !this.isForcedTheme_) {
      assert(this.systemTheme_ === SystemTheme.DEFAULT);

      this.appearanceBrowserProxy_.getThemeInfo(themeId).then(info => {
        this.themeSublabel_ = info.name;
      });

      this.themeUrl_ = 'https://chrome.google.com/webstore/detail/' + themeId;
      return;
    }

    this.themeUrl_ = '';

    if (themeId === AUTOGENERATED_THEME_ID || themeId === USER_COLOR_THEME_ID ||
        this.isForcedTheme_) {
      this.themeSublabel_ = this.i18n('chromeColors');
      return;
    }

    let i18nId;
    // <if expr="is_linux">
    switch (this.systemTheme_) {
      case SystemTheme.GTK:
        i18nId = 'gtkTheme';
        break;
      case SystemTheme.QT:
        i18nId = 'qtTheme';
        break;
      default:
        i18nId = 'classicTheme';
        break;
    }
    // </if>
    // <if expr="not is_linux">
    if (this.toolbarPinningEnabled_) {
      this.themeSublabel_ = '';
      return;
    }
    i18nId = 'chooseFromWebStore';
    // </if>
    this.themeSublabel_ = this.i18n(i18nId);
  }

  /** @return Whether applied theme is set by policy. */
  private computeIsForcedTheme_(): boolean {
    return !!this.getPref('autogenerated.theme.policy.color').controlledBy;
  }

  private async toolbarPinningStateChanged_(): Promise<void> {
    this.showResetPinnedActionsButton_ =
        !await this.appearanceBrowserProxy_.pinnedToolbarActionsAreDefault();
  }

  private isSelectedColorSchemeMode_(colorSchemeMode: ColorSchemeMode):
      boolean {
    return colorSchemeMode === this.selectedColorSchemeMode_;
  }

  private onColorSchemeModeChange_(): void {
    this.colorSchemeModeHandler_.setColorSchemeMode(
        parseInt(this.$.colorSchemeModeSelect.value, 10) as ColorSchemeMode);
  }

  private onZoomLevelChange_() {
    chrome.settingsPrivate.setDefaultZoom(parseFloat(this.$.zoomLevel.value));
  }

  /** @see blink::ZoomValuesEqual(). */
  private zoomValuesEqual_(zoom1: number, zoom2: number): boolean {
    return Math.abs(zoom1 - zoom2) <= 0.001;
  }

  private showHr_(previousIsVisible: boolean, nextIsVisible: boolean): boolean {
    return previousIsVisible && nextIsVisible;
  }

  private onHoverCardImagesToggleChange_(event: Event) {
    const enabled = (event.target as SettingsToggleButtonElement).checked;
    this.appearanceBrowserProxy_.recordHoverCardImagesEnabledChanged(enabled);
  }

  private onManagedDialogClosed_() {
    this.showManagedThemeDialog_ = false;
  }

  private onTabSearchPositionRestartClick_(e: Event) {
    // Prevent event from bubbling up to the toggle button.
    e.stopPropagation();
    this.performRestart(RestartType.RESTART);
  }

  private updateShowTabSearchRestartButton_(newValue: boolean): void {
    this.showTabSearchPositionRestartButton_ = newValue !==
        loadTimeData.getBoolean('tabSearchIsRightAlignedAtStartup');
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'settings-appearance-page': SettingsAppearancePageElement;
  }
}

customElements.define(
    SettingsAppearancePageElement.is, SettingsAppearancePageElement);