chromium/chrome/android/java/src/org/chromium/chrome/browser/dom_distiller/DistilledPagePrefsView.java

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.dom_distiller;

import android.content.Context;
import android.graphics.Typeface;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;

import org.chromium.chrome.R;
import org.chromium.components.dom_distiller.core.DistilledPagePrefs;
import org.chromium.dom_distiller.mojom.FontFamily;
import org.chromium.dom_distiller.mojom.Theme;

import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
 * A view which displays preferences for distilled pages.  This allows users
 * to change the theme, font size, etc. of distilled pages.
 */
public class DistilledPagePrefsView extends LinearLayout
        implements DistilledPagePrefs.Observer, SeekBar.OnSeekBarChangeListener {
    // XML layout for View.
    private static final int VIEW_LAYOUT = R.layout.distilled_page_prefs_view;

    // Buttons for color mode.
    private final Map<Integer /* Theme */, RadioButton> mColorModeButtons;

    private final NumberFormat mPercentageFormatter;

    // RadioGroup for color mode buttons.
    private RadioGroup mRadioGroup;

    private DistilledPagePrefs mDistilledPagePrefs;

    // Text field showing font scale percentage.
    private TextView mFontScaleTextView;

    // SeekBar for font scale. Has range of [0, 30].
    private SeekBar mFontScaleSeekBar;

    // Spinner for choosing a font family.
    private Spinner mFontFamilySpinner;

    /**
     * Creates a DistilledPagePrefsView.
     *
     * @param context Context for acquiring resources.
     * @param attrs Attributes from the XML layout inflation.
     */
    public DistilledPagePrefsView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mColorModeButtons = new HashMap<Integer /* Theme */, RadioButton>();
        mPercentageFormatter = NumberFormat.getPercentInstance(Locale.getDefault());
    }

    public static DistilledPagePrefsView create(
            Context context, DistilledPagePrefs distilledPagePrefs) {
        DistilledPagePrefsView prefsView =
                (DistilledPagePrefsView) LayoutInflater.from(context).inflate(VIEW_LAYOUT, null);
        prefsView.initDistilledPagePrefs(distilledPagePrefs);
        return prefsView;
    }

    @Override
    public void onFinishInflate() {
        super.onFinishInflate();
        mRadioGroup = findViewById(R.id.radio_button_group);
        mColorModeButtons.put(Theme.LIGHT, initializeAndGetButton(R.id.light_mode, Theme.LIGHT));
        mColorModeButtons.put(Theme.DARK, initializeAndGetButton(R.id.dark_mode, Theme.DARK));
        mColorModeButtons.put(Theme.SEPIA, initializeAndGetButton(R.id.sepia_mode, Theme.SEPIA));

        mFontScaleSeekBar = findViewById(R.id.font_size);
        mFontScaleTextView = findViewById(R.id.font_size_percentage);

        mFontFamilySpinner = findViewById(R.id.font_family);
    }

    private void initDistilledPagePrefs(DistilledPagePrefs distilledPagePrefs) {
        assert distilledPagePrefs != null;
        mDistilledPagePrefs = distilledPagePrefs;

        mColorModeButtons.get(mDistilledPagePrefs.getTheme()).setChecked(true);
        initFontFamilySpinner();

        // Setting initial progress on font scale seekbar.
        onChangeFontScaling(mDistilledPagePrefs.getFontScaling());
        mFontScaleSeekBar.setOnSeekBarChangeListener(this);
    }

    private void initFontFamilySpinner() {
        // These must be kept in sync (and in-order) with
        // components/dom_distiller/core/font_family_list.h
        // TODO(wychen): fix getStringArray issue (https://crbug/803117#c2)
        String[] fonts = {
            getResources().getString(R.string.sans_serif),
            getResources().getString(R.string.serif),
            getResources().getString(R.string.monospace)
        };
        ArrayAdapter<CharSequence> adapter =
                new ArrayAdapter<CharSequence>(
                        getContext(), android.R.layout.simple_spinner_item, fonts) {
                    @Override
                    public View getView(int position, View convertView, ViewGroup parent) {
                        View view = super.getView(position, convertView, parent);
                        return overrideTypeFace(view, position);
                    }

                    @Override
                    public View getDropDownView(int position, View convertView, ViewGroup parent) {
                        View view = super.getDropDownView(position, convertView, parent);
                        return overrideTypeFace(view, position);
                    }

                    private View overrideTypeFace(View view, int family) {
                        FontFamily.validate(family);
                        if (view instanceof TextView) {
                            TextView textView = (TextView) view;
                            if (family == FontFamily.MONOSPACE) {
                                textView.setTypeface(Typeface.MONOSPACE);
                            } else if (family == FontFamily.SANS_SERIF) {
                                textView.setTypeface(Typeface.SANS_SERIF);
                            } else if (family == FontFamily.SERIF) {
                                textView.setTypeface(Typeface.SERIF);
                            }
                        }
                        return view;
                    }
                };

        adapter.setDropDownViewResource(R.layout.distilled_page_font_family_spinner);
        mFontFamilySpinner.setAdapter(adapter);
        mFontFamilySpinner.setSelection(mDistilledPagePrefs.getFontFamily());
        mFontFamilySpinner.setOnItemSelectedListener(
                new OnItemSelectedListener() {
                    @Override
                    public void onItemSelected(
                            AdapterView<?> parent, View view, int family, long id) {
                        if (FontFamily.isKnownValue(family)) {
                            mDistilledPagePrefs.setFontFamily(family);
                        }
                    }

                    @Override
                    public void onNothingSelected(AdapterView<?> parent) {
                        // Nothing to do.
                    }
                });
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mRadioGroup.setOrientation(HORIZONTAL);

        for (RadioButton button : mColorModeButtons.values()) {
            ViewGroup.LayoutParams layoutParams = button.getLayoutParams();
            layoutParams.width = 0;
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // If text is wider than button, change layout so that buttons are stacked on
        // top of each other.
        for (RadioButton button : mColorModeButtons.values()) {
            if (button.getLineCount() > 1) {
                mRadioGroup.setOrientation(VERTICAL);
                for (RadioButton innerLoopButton : mColorModeButtons.values()) {
                    ViewGroup.LayoutParams layoutParams = innerLoopButton.getLayoutParams();
                    layoutParams.width = LayoutParams.MATCH_PARENT;
                }
                break;
            }
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        mDistilledPagePrefs.addObserver(this);
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mDistilledPagePrefs.removeObserver(this);
    }

    // DistilledPagePrefs.Observer

    @Override
    public void onChangeFontFamily(int fontFamily) {
        FontFamily.validate(fontFamily);
        mFontFamilySpinner.setSelection(fontFamily);
    }

    /** Changes which button is selected if the theme is changed in another tab. */
    @Override
    public void onChangeTheme(int theme) {
        Theme.validate(theme);
        mColorModeButtons.get(theme).setChecked(true);
    }

    @Override
    public void onChangeFontScaling(float scaling) {
        setFontScaleTextView(scaling);
        setFontScaleProgress(scaling);
    }

    // SeekBar.OnSeekBarChangeListener

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        // progress = [0, 30]
        // newValue = .50, .55, .60, ..., 1.95, 2.00 (supported font scales)
        float newValue = (progress / 20f + .5f);
        setFontScaleTextView(newValue);
        if (fromUser) {
            mDistilledPagePrefs.setFontScaling(newValue);
        }
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {}

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {}

    /** Initiatializes a Button and selects it if it corresponds to the current theme. */
    private RadioButton initializeAndGetButton(int id, final int theme) {
        Theme.validate(theme);
        final RadioButton button = findViewById(id);
        button.setOnClickListener(
                new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mDistilledPagePrefs.setTheme(theme);
                    }
                });
        return button;
    }

    /** Sets the progress of mFontScaleSeekBar. */
    private void setFontScaleProgress(float newValue) {
        // newValue = .50, .55, .60, ..., 1.95, 2.00 (supported font scales)
        // progress = [0, 30]
        int progress = (int) Math.round((newValue - .5) * 20);
        mFontScaleSeekBar.setProgress(progress);

        // On Android R+, use stateDescription so the only percentage announced to the user is
        // the scaling percent. For previous versions the SeekBar percentage is always announced.
        String userFriendlyFontDescription =
                getContext()
                        .getResources()
                        .getString(
                                R.string.font_size_accessibility_label,
                                mPercentageFormatter.format(newValue));
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            mFontScaleSeekBar.setStateDescription(userFriendlyFontDescription);
        } else {
            mFontScaleSeekBar.setContentDescription(userFriendlyFontDescription);
        }
    }

    /** Sets the text for the font scale text view. */
    private void setFontScaleTextView(float newValue) {
        mFontScaleTextView.setText(mPercentageFormatter.format(newValue));
    }
}