chromium/ui/android/java/src/org/chromium/ui/listmenu/BasicListMenu.java

// Copyright 2019 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.ui.listmenu;

import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;

import androidx.annotation.ColorRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;

import org.chromium.ui.R;
import org.chromium.ui.UiUtils;
import org.chromium.ui.modelutil.LayoutViewBuilder;
import org.chromium.ui.modelutil.MVCListAdapter.ListItem;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.ModelListAdapter;
import org.chromium.ui.modelutil.PropertyModel;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.LinkedList;
import java.util.List;

/**
 * An implementation of a list menu. Uses app_menu_layout as the default layout of menu and
 * list_menu_item as the default layout of a menu item.
 */
public class BasicListMenu implements ListMenu, OnItemClickListener {
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({ListMenuItemType.DIVIDER, ListMenuItemType.MENU_ITEM})
    public @interface ListMenuItemType {
        int DIVIDER = 0;
        int MENU_ITEM = 1;
    }

    /**
     * Helper function to build a ListItem of a divider.
     *
     * @return ListItem Representing a divider.
     */
    public static ListItem buildMenuDivider() {
        return new ListItem(ListMenuItemType.DIVIDER, new PropertyModel());
    }

    /**
     * Helper function to build a list menu item.
     *
     * @param title the title of the menu item.
     * @param contentDescription the a11y content description of the menu item.
     * @param groupId the group ID of the menu item. Pass 0 for none.
     * @param id the ID of the menu item. Pass 0 for none.
     * @param startIcon the start icon of the menu item. Pass 0 for none.
     * @param isIconTintable true if the icon should be tinted with the default tint.
     * @param groupContainsIcon true if the group the item belongs to contains an item with an icon.
     * @param enabled true if the menu item is enabled.
     * @param clickListener the click listener of the menu item.
     * @param intent the intent of the menu item.
     * @return ListItem representing a menu item.
     */
    public static ListItem buildListMenuItem(
            String title,
            @Nullable String contentDescription,
            int groupId,
            int id,
            @Nullable Drawable startIcon,
            boolean isIconTintable,
            boolean groupContainsIcon,
            boolean enabled,
            @Nullable View.OnClickListener clickListener,
            @Nullable Intent intent) {
        PropertyModel.Builder modelBuilder =
                new PropertyModel.Builder(ListMenuItemProperties.ALL_KEYS)
                        .with(ListMenuItemProperties.TITLE, title)
                        .with(ListMenuItemProperties.CONTENT_DESCRIPTION, contentDescription)
                        .with(ListMenuItemProperties.GROUP_ID, groupId)
                        .with(ListMenuItemProperties.MENU_ITEM_ID, id)
                        .with(ListMenuItemProperties.START_ICON_DRAWABLE, startIcon)
                        .with(ListMenuItemProperties.ENABLED, enabled)
                        .with(ListMenuItemProperties.CLICK_LISTENER, clickListener)
                        .with(ListMenuItemProperties.INTENT, intent)
                        .with(
                                ListMenuItemProperties.KEEP_START_ICON_SPACING_WHEN_HIDDEN,
                                groupContainsIcon)
                        .with(
                                ListMenuItemProperties.TEXT_APPEARANCE_ID,
                                R.style.TextAppearance_ListMenuItem)
                        .with(
                                ListMenuItemProperties.ICON_TINT_COLOR_STATE_LIST_ID,
                                isIconTintable ? R.color.list_menu_item_icon_color_list : 0);
        return new ListItem(ListMenuItemType.MENU_ITEM, modelBuilder.build());
    }

    private final @NonNull ListView mListView;
    private final @NonNull ModelListAdapter mAdapter;
    private final @NonNull View mContentView;
    private final @NonNull List<Runnable> mClickRunnables;
    private final @NonNull Delegate mDelegate;

    /**
     * @param context The {@link Context} to inflate the layout.
     * @param data Data representing the list items. All items in data are assumed to be enabled.
     * @param contentView The background of the list menu.
     * @param listView The {@link ListView} of the list menu.
     * @param delegate The {@link Delegate} that would be called when the menu is clicked.
     * @param backgroundTintColor The background tint color of the menu.
     */
    public BasicListMenu(
            @NonNull Context context,
            @NonNull ModelList data,
            @NonNull View contentView,
            @NonNull ListView listView,
            @NonNull Delegate delegate,
            @ColorRes int backgroundTintColor) {
        mAdapter = new ListMenuItemAdapter(data);
        mContentView = contentView;
        mListView = listView;
        mListView.setAdapter(mAdapter);
        mListView.setDivider(null);
        mListView.setOnItemClickListener(this);
        mDelegate = delegate;
        mClickRunnables = new LinkedList<>();
        registerListItemTypes();

        if (backgroundTintColor != 0) {
            ViewCompat.setBackgroundTintList(
                    mContentView,
                    ColorStateList.valueOf(ContextCompat.getColor(context, backgroundTintColor)));
        }
    }

    @NonNull
    @Override
    public View getContentView() {
        return mContentView;
    }

    @NonNull
    public ListView getListView() {
        return mListView;
    }

    @Override
    public void addContentViewClickRunnable(Runnable runnable) {
        mClickRunnables.add(runnable);
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        mDelegate.onItemSelected(((ListItem) mAdapter.getItem(position)).model);
        for (Runnable r : mClickRunnables) {
            r.run();
        }
    }

    @Override
    public int getMaxItemWidth() {
        return UiUtils.computeListAdapterContentDimensions(mAdapter, mListView)[0];
    }

    /**
     * Returns the measured width and height of the menu.
     *
     * @return an array with the menu's width stored at index 0, and the height stored at index 1.
     */
    public int[] getMenuDimensions() {
        int[] result = UiUtils.computeListAdapterContentDimensions(mAdapter, mListView);
        int horizontalPadding = mContentView.getPaddingLeft() + mContentView.getPaddingRight();
        int verticalPadding = mContentView.getPaddingTop() + mContentView.getPaddingBottom();
        result[0] += horizontalPadding;
        result[1] += verticalPadding;
        return result;
    }

    private void registerListItemTypes() {
        mAdapter.registerType(
                ListMenuItemType.MENU_ITEM,
                new LayoutViewBuilder(R.layout.list_menu_item),
                ListMenuItemViewBinder::binder);
        mAdapter.registerType(
                ListMenuItemType.DIVIDER,
                new LayoutViewBuilder(R.layout.list_section_divider),
                ListSectionDividerViewBinder::bind);
    }
}