chromium/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListEditorActionViewLayout.java

// Copyright 2022 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.tasks.tab_management;

import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;

import androidx.collection.ArraySet;

import org.chromium.base.MathUtils;
import org.chromium.chrome.tab_ui.R;
import org.chromium.components.browser_ui.widget.NumberRollView;
import org.chromium.ui.listmenu.ListMenuButton;
import org.chromium.ui.listmenu.ListMenuButtonDelegate;

import java.util.ArrayList;
import java.util.Set;

/**
 * A {@link LinearLayout} that displays only the TabListEditorMenuItem ActionViews that fit in
 * the space it contains. Managed by a {@link TabListEditorMenu}.
 */
public class TabListEditorActionViewLayout extends LinearLayout {
    /** All {@link TabListEditoreMenuItem} action views with menu items. */
    private final ArrayList<TabListEditorMenuItem> mMenuItemsWithActionView =
            new ArrayList<>();

    /** The {@link TabListEditoreMenuItem}s with visible action views. */
    private final Set<TabListEditorMenuItem> mVisibleActions = new ArraySet<>();

    /** {@link ListMenuButton} for showing the {@link TabListEditorMenu}. */
    private ListMenuButton mMenuButton;

    private LinearLayout.LayoutParams mActionViewParams;

    private Context mContext;
    private ActionViewLayoutDelegate mDelegate;
    private boolean mHasMenuOnlyItems;

    /** Delegate updates in response to which action views are visible. */
    public interface ActionViewLayoutDelegate {
        /**
         * @param visibleActions the list of {@link TabListEditorMenuItem}s with visible action
         * views.
         */
        public void setVisibleActionViews(Set<TabListEditorMenuItem> visibleActions);
    }

    public TabListEditorActionViewLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mActionViewParams =
                new LinearLayout.LayoutParams(
                        LinearLayout.LayoutParams.WRAP_CONTENT,
                        LinearLayout.LayoutParams.WRAP_CONTENT,
                        0.0f);
        mActionViewParams.gravity = Gravity.CENTER_VERTICAL;
        mContext = context;
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMenuButton = findViewById(R.id.list_menu_button);
        mMenuButton.tryToFitLargestItem(true);
    }

    ListMenuButton getListMenuButtonForTesting() {
        return mMenuButton;
    }

    /**
     * @param delegate for handling menu button presses.
     */
    public void setListMenuButtonDelegate(ListMenuButtonDelegate delegate) {
        mMenuButton.setDelegate(delegate);
    }

    /**
     * @param delegate the delegate to notify of action view changes.
     */
    public void setActionViewLayoutDelegate(ActionViewLayoutDelegate delegate) {
        mDelegate = delegate;
    }

    /**
     * @param hasMenuOnlyItems whether the menu has items which are only shown in the menu.
     */
    public void setHasMenuOnlyItems(boolean hasMenuOnlyItems) {
        mHasMenuOnlyItems = hasMenuOnlyItems;
        update();
    }

    /**
     * @param menuItem a menu item with an action view to attempt to show prioritized by insertion
     * order.
     */
    public void add(TabListEditorMenuItem menuItem) {
        assert menuItem.getActionView() != null;

        mMenuItemsWithActionView.add(menuItem);
        update();
    }

    /** Clears the action views from this layout. */
    public void clear() {
        removeAllActionViews();
        mMenuItemsWithActionView.clear();
        mHasMenuOnlyItems = false;
        mMenuButton.setVisibility(View.GONE);
        update();
    }

    /** Dismisses the menu. */
    public void dismissMenu() {
        mMenuButton.dismiss();
    }

    private void removeAllActionViews() {
        for (TabListEditorMenuItem menuItem : mMenuItemsWithActionView) {
            final View actionView = menuItem.getActionView();
            if (this == actionView.getParent()) {
                removeView(menuItem.getActionView());
            }
        }
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Get empty size without action views.
        removeAllActionViews();
        mMenuButton.setVisibility(View.VISIBLE);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        final int width = getMeasuredWidth();

        // The width that is required by visible views.
        int requiredWidth = getPaddingLeft() + getPaddingRight();
        // The width including the menu button assuming it is visible.
        int usedWidth = requiredWidth + mMenuButton.getMeasuredWidth();

        mVisibleActions.clear();

        // Add all action views that fit.
        boolean hasForcedAnyActionViewToMenu = false;
        final int childMeasureSpec =
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        for (TabListEditorMenuItem menuItem : mMenuItemsWithActionView) {
            final View actionView = menuItem.getActionView();
            actionView.measure(childMeasureSpec, childMeasureSpec);
            final int actionViewWidth = actionView.getMeasuredWidth();
            if (usedWidth + actionViewWidth > width || hasForcedAnyActionViewToMenu) {
                // The ActionView doesn't fit. Ensure it still has a LayoutParams.
                actionView.setLayoutParams(mActionViewParams);
                hasForcedAnyActionViewToMenu = true;
                continue;
            }

            // Add views in front of the menu button.
            addView(actionView, getChildCount() - 1, mActionViewParams);
            mVisibleActions.add(menuItem);
            usedWidth += actionViewWidth;
            requiredWidth += actionViewWidth;
        }
        if (mDelegate != null) {
            // Any items in mVisibleActions will appear in the Toolbar. The remaining items will be
            // forced into the overflow menu.
            mDelegate.setVisibleActionViews(mVisibleActions);
        }
        if (mHasMenuOnlyItems || hasForcedAnyActionViewToMenu) {
            mMenuButton.setVisibility(View.VISIBLE);
            requiredWidth += mMenuButton.getMeasuredWidth();
        } else {
            mMenuButton.setVisibility(View.GONE);
        }

        // Make the number roll view use the remaining space.
        makeNumberRollViewFill(MathUtils.clamp(width - requiredWidth, 0, width));

        // Get the final measurement.
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    private void makeNumberRollViewFill(int maxWidth) {
        View firstView = getChildAt(0);
        if (firstView instanceof NumberRollView) {
            NumberRollView numberRollView = (NumberRollView) firstView;
            LinearLayout.LayoutParams params =
                    (LinearLayout.LayoutParams) numberRollView.getLayoutParams();
            params.width = maxWidth;
            numberRollView.setLayoutParams(params);
        }
    }

    private void update() {
        int widthMeasureSpec =
                View.MeasureSpec.makeMeasureSpec(getMeasuredWidth(), View.MeasureSpec.AT_MOST);
        int heightMeasureSpec =
                View.MeasureSpec.makeMeasureSpec(getMeasuredHeight(), View.MeasureSpec.EXACTLY);
        measure(widthMeasureSpec, heightMeasureSpec);
    }
}