chromium/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ContextMenuChipController.java

// Copyright 2020 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.contextmenu;

import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.view.LayoutInflater;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import org.chromium.chrome.R;
import org.chromium.components.browser_ui.widget.chips.ChipView;
import org.chromium.components.embedder_support.contextmenu.ChipRenderParams;
import org.chromium.ui.text.SpanApplier;
import org.chromium.ui.text.SpanApplier.SpanInfo;
import org.chromium.ui.widget.AnchoredPopupWindow;
import org.chromium.ui.widget.ViewRectProvider;

/** A controller to handle chip construction and cross-app communication. */
class ContextMenuChipController implements View.OnClickListener {
    private boolean mFakeLensQueryResultForTesting;
    private View mAnchorView;
    private ChipView mChipView;
    private AnchoredPopupWindow mPopupWindow;
    private Context mContext;
    private ChipRenderParams mChipRenderParams;
    private final Runnable mDismissContextMenuCallback;

    ContextMenuChipController(
            Context context, View anchorView, final Runnable dismissContextMenuCallback) {
        mContext = context;
        mAnchorView = anchorView;
        mDismissContextMenuCallback = dismissContextMenuCallback;
    }

    /**
     * Returns the necessary px necessary to render the chip with enough margin space above and
     * below.
     */
    int getVerticalPxNeededForChip() {
        return 2
                        * mContext.getResources()
                                .getDimensionPixelSize(R.dimen.context_menu_chip_vertical_margin)
                + mContext.getResources().getDimensionPixelSize(R.dimen.chip_default_height);
    }

    /**
     * Derive max text view width by subtracting the width of other elements from the max chip
     * width.
     */
    @VisibleForTesting
    int getChipTextMaxWidthPx(boolean isRemoveIconHidden) {
        int maxWidthPx =
                mContext.getResources().getDimensionPixelSize(R.dimen.context_menu_chip_max_width)
                        // Padding before primary icon
                        - mContext.getResources()
                                .getDimensionPixelSize(
                                        R.dimen.chip_element_extended_leading_padding)
                        // Padding after primary icon
                        - mContext.getResources()
                                .getDimensionPixelSize(R.dimen.chip_element_leading_padding)
                        // Primary icon width.
                        - mContext.getResources()
                                .getDimensionPixelSize(R.dimen.context_menu_chip_icon_size);
        if (!isRemoveIconHidden) {
            maxWidthPx =
                    maxWidthPx
                            // Padding before close icon.
                            - mContext.getResources()
                                    .getDimensionPixelSize(
                                            R.dimen.chip_end_icon_extended_margin_start)
                            // End icon width.
                            - mContext.getResources()
                                    .getDimensionPixelSize(R.dimen.context_menu_chip_icon_size)
                            // Padding after close icon.
                            - mContext.getResources()
                                    .getDimensionPixelSize(
                                            R.dimen.chip_extended_end_padding_with_end_icon);
        }

        return maxWidthPx;
    }

    // This method should only be used in test files.  It is not marked
    // @VisibleForTesting to allow the Coordinator to reference it in its
    // own testing methods.
    void setFakeLensQueryResultForTesting() {
        mFakeLensQueryResultForTesting = true;
    }

    @Override
    public void onClick(View v) {
        if (v == mChipView) {
            // The onClick callback may result in a cross-app switch so dismiss the menu before
            // executing that logic. Also note that dismissing the menu will also dismiss the chip.
            mDismissContextMenuCallback.run();
            mChipRenderParams.onClickCallback.run();
        }
    }

    /**
     * Dismiss the lens chip. Needed for cases where a user dismisses
     * the context menu without closing the chip manually.
     */
    void dismissChipIfShowing() {
        if (mPopupWindow != null && mPopupWindow.isShowing()) {
            mPopupWindow.dismiss();
        }
    }

    /**
     * Inflate an anchored chip view, set it up, and show it to the user.
     * @param chipRenderParams The data to construct the chip.
     */
    protected void showChip(@NonNull ChipRenderParams chipRenderParams) {
        if (mPopupWindow != null) {
            // Chip has already been shown for this context menu.
            return;
        }

        mChipRenderParams = chipRenderParams;

        mChipView =
                (ChipView) LayoutInflater.from(mContext).inflate(R.layout.context_menu_chip, null);

        ViewRectProvider rectProvider = new ViewRectProvider(mAnchorView);
        // Draw a clear background to avoid blocking context menu items.
        mPopupWindow =
                new AnchoredPopupWindow(
                        mContext,
                        mAnchorView,
                        new ColorDrawable(Color.TRANSPARENT),
                        mChipView,
                        rectProvider);
        mPopupWindow.setAnimationStyle(R.style.ChipAnimation);
        mPopupWindow.setPreferredHorizontalOrientation(
                AnchoredPopupWindow.HorizontalOrientation.CENTER);
        // The bottom margin will determine the vertical placement of the chip, so
        // ensure that this distance is computed from the anchor.
        mPopupWindow.setPreferredVerticalOrientation(AnchoredPopupWindow.VerticalOrientation.ABOVE);
        mPopupWindow.setFocusable(false);
        // Don't dismiss as a result of touches outside of the chip popup.
        mPopupWindow.setOutsideTouchable(false);
        mPopupWindow.setMaxWidth(
                mContext.getResources().getDimensionPixelSize(R.dimen.context_menu_chip_max_width));

        mChipView
                .getPrimaryTextView()
                .setText(
                        SpanApplier.removeSpanText(
                                mContext.getString(chipRenderParams.titleResourceId),
                                new SpanInfo("<new>", "</new>")));
        // TODO(benwgold): Consult with Chrome UX owners to see if Chip UI hierarchy should be
        // refactored.
        mChipView
                .getPrimaryTextView()
                .setMaxWidth(getChipTextMaxWidthPx(chipRenderParams.isRemoveIconHidden));

        if (chipRenderParams.iconResourceId != 0) {
            mChipView.setIcon(chipRenderParams.iconResourceId, false);
        }

        if (!chipRenderParams.isRemoveIconHidden) {
            mChipView.addRemoveIcon();
            mChipView.setRemoveIconClickListener(
                    v -> {
                        dismissChipIfShowing();
                    });
        }

        mChipView.setOnClickListener(this);

        mPopupWindow.show();
        if (mChipRenderParams.onShowCallback != null) {
            mChipRenderParams.onShowCallback.run();
        }
    }

    // This method should only be used in test files.  It is not marked
    // @VisibleForTesting to allow the Coordinator to reference it in its
    // own testing methods.
    AnchoredPopupWindow getCurrentPopupWindowForTesting() {
        return mPopupWindow;
    }

    // This method should only be used in test files.  It is not marked
    // @VisibleForTesting to allow the Coordinator to reference it in its
    // own testing methods.
    void clickChipForTesting() {
        onClick(mChipView);
    }
}