chromium/components/embedder_support/android/java/src/org/chromium/components/embedder_support/delegate/ColorPickerCoordinator.java

// Copyright 2024 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.components.embedder_support.delegate;

import static org.chromium.components.embedder_support.delegate.ColorPickerProperties.CHOSEN_COLOR;
import static org.chromium.components.embedder_support.delegate.ColorPickerProperties.CHOSEN_SUGGESTION_INDEX;
import static org.chromium.components.embedder_support.delegate.ColorPickerProperties.CUSTOM_COLOR_PICKED_CALLBACK;
import static org.chromium.components.embedder_support.delegate.ColorPickerProperties.DIALOG_DISMISSED_CALLBACK;
import static org.chromium.components.embedder_support.delegate.ColorPickerProperties.IS_ADVANCED_VIEW;
import static org.chromium.components.embedder_support.delegate.ColorPickerProperties.MAKE_CHOICE_CALLBACK;
import static org.chromium.components.embedder_support.delegate.ColorPickerProperties.SUGGESTIONS_ADAPTER;
import static org.chromium.components.embedder_support.delegate.ColorPickerProperties.SUGGESTIONS_NUM_COLUMNS;
import static org.chromium.components.embedder_support.delegate.ColorPickerProperties.VIEW_SWITCHED_CALLBACK;

import android.content.Context;
import android.graphics.Color;

import androidx.annotation.NonNull;

import org.chromium.base.Callback;
import org.chromium.content_public.browser.util.DialogTypeRecorder;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.ModelListAdapter;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;

import java.util.ArrayList;
import java.util.List;

/**
 * This class enables the creation of the color picker dialog and make the decisions within the
 * component. Apart from calculating and creating the columns and creating the suggestion colors, it
 * also handles the switches between simple and advanced views.
 */
public class ColorPickerCoordinator {
    // Default color suggestions, overridden if a single suggestion is provided by the web.
    private static final int[] DEFAULT_COLORS = {
        Color.RED,
        Color.CYAN,
        Color.BLUE,
        Color.GREEN,
        Color.MAGENTA,
        Color.YELLOW,
        Color.BLACK,
        Color.WHITE
    };

    private static final int[] DEFAULT_COLOR_LABEL_IDS = {
        R.string.color_picker_button_red,
        R.string.color_picker_button_cyan,
        R.string.color_picker_button_blue,
        R.string.color_picker_button_green,
        R.string.color_picker_button_magenta,
        R.string.color_picker_button_yellow,
        R.string.color_picker_button_black,
        R.string.color_picker_button_white
    };

    private static final int MAX_NUMBER_OF_COLUMNS = 5;

    private int mInitialColor;
    private final Context mContext;
    private final Callback<Integer> mDialogDismissedCallback;
    private final ColorPickerDialogView mColorPickerDialogView;
    private List<ColorSuggestion> mSuggestions;
    private PropertyModel mModel;

    private MVCListAdapter.ModelList mSuggestionsModelList;
    private ModelListAdapter mSuggestionsAdapter;

    static ColorPickerCoordinator create(
            @NonNull Context context, Callback<Integer> dialogDismissedCallback) {
        ColorPickerDialogView dialogView = new ColorPickerDialogView(context);
        return new ColorPickerCoordinator(context, dialogDismissedCallback, dialogView);
    }

    public ColorPickerCoordinator(
            @NonNull Context context,
            @NonNull Callback<Integer> dialogDismissedCallback,
            @NonNull ColorPickerDialogView dialogView) {
        mContext = context;
        mDialogDismissedCallback = dialogDismissedCallback;
        mSuggestions = new ArrayList<>();
        mColorPickerDialogView = dialogView;
    }

    void show(int initialColor) {
        mInitialColor = initialColor;

        if (mSuggestions.isEmpty()) {
            createDefaultSuggestions();
        }

        // Construct a ModelListAdapter for the given suggestions.
        generateSuggestionsModelList();
        mSuggestionsAdapter = new ModelListAdapter(mSuggestionsModelList);
        mSuggestionsAdapter.registerType(
                ColorPickerSuggestionProperties.ListItemType.DEFAULT,
                ColorPickerViewBinder::buildView,
                ColorPickerViewBinder::bindAdapter);

        mModel =
                new PropertyModel.Builder(ColorPickerProperties.ALL_KEYS)
                        .with(CHOSEN_COLOR, initialColor)
                        .with(CHOSEN_SUGGESTION_INDEX, -1)
                        .with(SUGGESTIONS_NUM_COLUMNS, calculateNumberOfColumns())
                        .with(SUGGESTIONS_ADAPTER, mSuggestionsAdapter)
                        .with(IS_ADVANCED_VIEW, false)
                        .with(CUSTOM_COLOR_PICKED_CALLBACK, this::handleCustomColorPicked)
                        .with(VIEW_SWITCHED_CALLBACK, this::handleViewSwitched)
                        .with(MAKE_CHOICE_CALLBACK, this::handleMakeChoice)
                        .with(DIALOG_DISMISSED_CALLBACK, mDialogDismissedCallback)
                        .build();

        PropertyModelChangeProcessor.create(
                mModel, mColorPickerDialogView, ColorPickerViewBinder::bind);

        mColorPickerDialogView.show();
        DialogTypeRecorder.recordDialogType(DialogTypeRecorder.DialogType.COLOR_PICKER);
    }

    void close() {
        mColorPickerDialogView.dismiss();
    }

    public void addColorSuggestion(int color, String label) {
        mSuggestions.add(new ColorSuggestion(color, label));
    }

    private void createDefaultSuggestions() {
        assert mSuggestions.isEmpty();
        assert DEFAULT_COLORS.length == DEFAULT_COLOR_LABEL_IDS.length;
        for (int i = 0; i < DEFAULT_COLORS.length; i++) {
            mSuggestions.add(
                    new ColorSuggestion(
                            DEFAULT_COLORS[i], mContext.getString(DEFAULT_COLOR_LABEL_IDS[i])));
        }
    }

    private void generateSuggestionsModelList() {
        assert !mSuggestions.isEmpty();
        mSuggestionsModelList = new MVCListAdapter.ModelList();
        for (int i = 0; i < mSuggestions.size(); i++) {
            ColorSuggestion suggestion = mSuggestions.get(i);
            PropertyModel itemModel =
                    new PropertyModel.Builder(ColorPickerSuggestionProperties.ALL_KEYS)
                            .with(ColorPickerSuggestionProperties.INDEX, i)
                            .with(ColorPickerSuggestionProperties.COLOR, suggestion.mColor)
                            .with(ColorPickerSuggestionProperties.LABEL, suggestion.mLabel)
                            .with(ColorPickerSuggestionProperties.IS_SELECTED, false)
                            .with(
                                    ColorPickerSuggestionProperties.ONCLICK,
                                    this::handleSuggestionColorPicked)
                            .build();
            mSuggestionsModelList.add(
                    new MVCListAdapter.ListItem(
                            ColorPickerSuggestionProperties.ListItemType.DEFAULT, itemModel));
        }
    }

    private int calculateNumberOfColumns() {
        assert !mSuggestions.isEmpty();
        if (mSuggestions.size() <= MAX_NUMBER_OF_COLUMNS) {
            return mSuggestions.size();
        } else {
            return Math.min(
                    MAX_NUMBER_OF_COLUMNS,
                    // Since we want to show two rows of equal number of columns,
                    // we need an even number. This calculation is for if
                    // mSuggestions.size() is an uneven number,
                    (int) mSuggestions.size() / 2 + (mSuggestions.size() % 2));
        }
    }

    private void handleSuggestionColorPicked(int index) {
        // Remove previous selection if present.
        if (mModel.get(CHOSEN_SUGGESTION_INDEX) != -1) {
            mSuggestionsModelList
                    .get(mModel.get(CHOSEN_SUGGESTION_INDEX))
                    .model
                    .set(ColorPickerSuggestionProperties.IS_SELECTED, false);
        }
        mSuggestionsModelList
                .get(index)
                .model
                .set(ColorPickerSuggestionProperties.IS_SELECTED, true);
        mModel.set(CHOSEN_SUGGESTION_INDEX, index);
        mModel.set(
                CHOSEN_COLOR,
                mSuggestionsModelList.get(index).model.get(ColorPickerSuggestionProperties.COLOR));
    }

    private void handleCustomColorPicked(int newColor) {
        // Set chosen suggestion index back to default value of -1 if present, and update the
        // IS_SELECTED value of whatever suggestion was picked (if present).
        if (mModel.get(CHOSEN_SUGGESTION_INDEX) != -1) {
            mSuggestionsModelList
                    .get(mModel.get(CHOSEN_SUGGESTION_INDEX))
                    .model
                    .set(ColorPickerSuggestionProperties.IS_SELECTED, false);
            mModel.set(CHOSEN_SUGGESTION_INDEX, -1);
        }
        mModel.set(CHOSEN_COLOR, newColor);
    }

    private void handleViewSwitched(Void unused) {
        mModel.set(IS_ADVANCED_VIEW, !mModel.get(IS_ADVANCED_VIEW));
    }

    private void handleMakeChoice(boolean chosen) {
        if (chosen) {
            mDialogDismissedCallback.onResult(mModel.get(CHOSEN_COLOR));
        } else {
            mDialogDismissedCallback.onResult(mInitialColor);
        }
    }
}