[go: up one dir, main page]

blob: e3f817c49bc0ae60961211584308aca167707932 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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 static org.chromium.chrome.browser.contextmenu.RevampedContextMenuItemProperties.MENU_ID;
import static org.chromium.chrome.browser.contextmenu.RevampedContextMenuItemWithIconButtonProperties.BUTTON_CLICK_LISTENER;
import static org.chromium.chrome.browser.contextmenu.RevampedContextMenuItemWithIconButtonProperties.BUTTON_MENU_ID;
import android.app.Activity;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewStub;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import org.chromium.base.Callback;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.performance_hints.PerformanceHintsObserver;
import org.chromium.chrome.browser.performance_hints.PerformanceHintsObserver.PerformanceClass;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.browser_ui.widget.ContextMenuDialog;
import org.chromium.components.embedder_support.contextmenu.ContextMenuParams;
import org.chromium.content_public.browser.LoadCommittedDetails;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.ui.base.MenuSourceType;
import org.chromium.ui.base.WindowAndroid;
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 org.chromium.ui.widget.AnchoredPopupWindow;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
* The main coordinator for the Revamped Context Menu, responsible for creating the context menu in
* general and the header component.
*/
public class RevampedContextMenuCoordinator implements ContextMenuUi {
@Retention(RetentionPolicy.SOURCE)
@IntDef({ListItemType.DIVIDER, ListItemType.HEADER, ListItemType.CONTEXT_MENU_ITEM,
ListItemType.CONTEXT_MENU_ITEM_WITH_ICON_BUTTON})
public @interface ListItemType {
int DIVIDER = 0;
int HEADER = 1;
int CONTEXT_MENU_ITEM = 2;
int CONTEXT_MENU_ITEM_WITH_ICON_BUTTON = 3;
}
private static final int INVALID_ITEM_ID = -1;
private WebContents mWebContents;
private WebContentsObserver mWebContentsObserver;
private RevampedContextMenuChipController mChipController;
private RevampedContextMenuHeaderCoordinator mHeaderCoordinator;
private RevampedContextMenuListView mListView;
private float mTopContentOffsetPx;
private ContextMenuDialog mDialog;
private Runnable mOnMenuClosed;
private ContextMenuNativeDelegate mNativeDelegate;
/**
* Constructor that also sets the content offset.
*
* @param topContentOffsetPx content offset from the top.
* @param nativeDelegate The {@link ContextMenuNativeDelegate} to retrieve the thumbnail from
* native.
*/
RevampedContextMenuCoordinator(
float topContentOffsetPx, ContextMenuNativeDelegate nativeDelegate) {
mTopContentOffsetPx = topContentOffsetPx;
mNativeDelegate = nativeDelegate;
}
@Override
public void displayMenu(final WindowAndroid window, WebContents webContents,
ContextMenuParams params, List<Pair<Integer, ModelList>> items,
Callback<Integer> onItemClicked, final Runnable onMenuShown,
final Runnable onMenuClosed) {
displayMenuWithChip(window, webContents, params, items, onItemClicked, onMenuShown,
onMenuClosed, /* chipDelegate=*/null);
}
@Override
public void dismiss() {
dismissDialog();
}
// Shows the menu with chip.
void displayMenuWithChip(final WindowAndroid window, WebContents webContents,
ContextMenuParams params, List<Pair<Integer, ModelList>> items,
Callback<Integer> onItemClicked, final Runnable onMenuShown,
final Runnable onMenuClosed, @Nullable ChipDelegate chipDelegate) {
mOnMenuClosed = onMenuClosed;
final boolean isPopup = params.getSourceType() == MenuSourceType.MENU_SOURCE_MOUSE;
Activity activity = window.getActivity().get();
final float density = activity.getResources().getDisplayMetrics().density;
final float touchPointXPx = params.getTriggeringTouchXDp() * density;
final float touchPointYPx = params.getTriggeringTouchYDp() * density;
int dialogTopMarginPx = ContextMenuDialog.NO_CUSTOM_MARGIN;
int dialogBottomMarginPx = ContextMenuDialog.NO_CUSTOM_MARGIN;
final View layout = LayoutInflater.from(activity).inflate(
R.layout.context_menu_fullscreen_container, null);
// Only display a chip if an image was selected and the menu isn't a popup.
if (params.isImage() && chipDelegate != null && !isPopup) {
View chipAnchorView = layout.findViewById(R.id.context_menu_chip_anchor_point);
mChipController = new RevampedContextMenuChipController(activity, chipAnchorView);
chipDelegate.getChipRenderParams((chipRenderParams) -> {
if (chipDelegate.isValidChipRenderParams(chipRenderParams) && mDialog.isShowing()) {
mChipController.showChip(chipRenderParams);
}
});
dialogBottomMarginPx = mChipController.getVerticalPxNeededForChip();
// Allow dialog to get close to the top of the screen.
dialogTopMarginPx = dialogBottomMarginPx / 2;
}
final View menu = isPopup
? LayoutInflater.from(activity).inflate(R.layout.context_menu, null)
: ((ViewStub) layout.findViewById(R.id.context_menu_stub)).inflate();
mDialog = createContextMenuDialog(activity, layout, menu, isPopup, touchPointXPx,
touchPointYPx, dialogTopMarginPx, dialogBottomMarginPx);
mDialog.setOnShowListener(dialogInterface -> onMenuShown.run());
mDialog.setOnDismissListener(dialogInterface -> mOnMenuClosed.run());
mWebContents = webContents;
int performanceClass = params.isAnchor()
? PerformanceHintsObserver.getPerformanceClassForURL(
webContents, params.getLinkUrl())
: PerformanceClass.PERFORMANCE_UNKNOWN;
mHeaderCoordinator = new RevampedContextMenuHeaderCoordinator(activity, performanceClass,
params, Profile.fromWebContents(mWebContents), mNativeDelegate);
// The Integer here specifies the {@link ListItemType}.
ModelList listItems = getItemList(activity, items, onItemClicked);
ModelListAdapter adapter = new ModelListAdapter(listItems) {
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public boolean isEnabled(int position) {
return getItemViewType(position) == ListItemType.CONTEXT_MENU_ITEM
|| getItemViewType(position)
== ListItemType.CONTEXT_MENU_ITEM_WITH_ICON_BUTTON;
}
@Override
public long getItemId(int position) {
if (getItemViewType(position) == ListItemType.CONTEXT_MENU_ITEM
|| getItemViewType(position)
== ListItemType.CONTEXT_MENU_ITEM_WITH_ICON_BUTTON) {
return ((ListItem) getItem(position)).model.get(MENU_ID);
}
return INVALID_ITEM_ID;
}
};
mListView = menu.findViewById(R.id.context_menu_list_view);
mListView.setAdapter(adapter);
// Note: clang-format does a bad job formatting lambdas so we turn it off here.
// clang-format off
adapter.registerType(
ListItemType.HEADER,
new LayoutViewBuilder(R.layout.revamped_context_menu_header),
RevampedContextMenuHeaderViewBinder::bind);
adapter.registerType(
ListItemType.DIVIDER,
new LayoutViewBuilder(R.layout.app_menu_divider),
(m, v, p) -> {});
adapter.registerType(
ListItemType.CONTEXT_MENU_ITEM,
new LayoutViewBuilder(R.layout.revamped_context_menu_row),
RevampedContextMenuItemViewBinder::bind);
adapter.registerType(
ListItemType.CONTEXT_MENU_ITEM_WITH_ICON_BUTTON,
new LayoutViewBuilder(R.layout.revamped_context_menu_share_row),
RevampedContextMenuItemWithIconButtonViewBinder::bind);
// clang-format on
mListView.setOnItemClickListener((p, v, pos, id) -> {
assert id != INVALID_ITEM_ID;
clickItem((int) id, activity, onItemClicked);
});
mWebContentsObserver = new WebContentsObserver(mWebContents) {
@Override
public void navigationEntryCommitted(LoadCommittedDetails details) {
dismissDialog();
}
};
mDialog.show();
}
/**
* Execute an action for the selected item and close the menu.
* @param id The id of the item.
* @param activity The current activity.
* @param onItemClicked The callback to take action with the given id.
*/
private void clickItem(int id, Activity activity, Callback<Integer> onItemClicked) {
// Do not start any action when the activity is on the way to destruction.
// See https://crbug.com/990987
if (activity.isFinishing() || activity.isDestroyed()) return;
onItemClicked.onResult((int) id);
dismissDialog();
}
/**
* Returns the fully complete dialog based off the params and the itemGroups.
*
* @param activity Used to inflate the dialog.
* @param layout The inflated context menu layout that will house the context menu.
* @param view The inflated view that contains the list view.
* @param isPopup Whether the context menu is being shown in a {@link AnchoredPopupWindow}.
* @param touchPointXPx The x-coordinate of the touch that triggered the context menu.
* @param touchPointYPx The y-coordinate of the touch that triggered the context menu.
* @param topMarginPx An explicit top margin for the dialog, or -1 to use default
* defined in XML.
* @param bottomMarginPx An explicit bottom margin for the dialog, or -1 to use default
* defined in XML.
* @return Returns a final dialog that does not have a background can be displayed using
* {@link AlertDialog#show()}.
*/
private ContextMenuDialog createContextMenuDialog(Activity activity, View layout, View view,
boolean isPopup, float touchPointXPx, float touchPointYPx, int topMarginPx,
int bottomMarginPx) {
// TODO(sinansahin): Refactor ContextMenuDialog as well.
final ContextMenuDialog dialog = new ContextMenuDialog(activity,
R.style.Theme_Chromium_AlertDialog, touchPointXPx, touchPointYPx,
mTopContentOffsetPx, topMarginPx, bottomMarginPx, layout, view, isPopup);
dialog.setContentView(layout);
return dialog;
}
@VisibleForTesting
ModelList getItemList(Activity activity, List<Pair<Integer, ModelList>> items,
Callback<Integer> onItemClicked) {
ModelList itemList = new ModelList();
// Start with the header
itemList.add(new ListItem(ListItemType.HEADER, mHeaderCoordinator.getModel()));
for (Pair<Integer, ModelList> group : items) {
// Add a divider
itemList.add(new ListItem(ListItemType.DIVIDER, new PropertyModel()));
// Add the items in the group
itemList.addAll(group.second);
}
for (ListItem item : itemList) {
if (item.type == ListItemType.CONTEXT_MENU_ITEM_WITH_ICON_BUTTON) {
item.model.set(BUTTON_CLICK_LISTENER,
(v) -> clickItem(item.model.get(BUTTON_MENU_ID), activity, onItemClicked));
}
}
return itemList;
}
private void dismissDialog() {
if (mWebContentsObserver != null) {
mWebContentsObserver.destroy();
}
if (mChipController != null) {
mChipController.dismissLensChipIfShowing();
}
mDialog.dismiss();
}
@VisibleForTesting
Callback<ChipRenderParams> getChipRenderParamsCallbackForTesting(ChipDelegate chipDelegate) {
return (chipRenderParams) -> {
if (chipDelegate.isValidChipRenderParams(chipRenderParams) && mDialog.isShowing()) {
mChipController.showChip(chipRenderParams);
}
};
}
@VisibleForTesting
void initializeHeaderCoordinatorForTesting(Activity activity, ContextMenuParams params,
Profile profile, ContextMenuNativeDelegate nativeDelegate) {
mHeaderCoordinator = new RevampedContextMenuHeaderCoordinator(
activity, PerformanceClass.PERFORMANCE_UNKNOWN, params, profile, nativeDelegate);
}
@VisibleForTesting
void simulateShoppyImageClassificationForTesting() {
// Don't need to initialize controller because that should be triggered by
// forcing feature flags.
mChipController.setFakeLensQueryResultForTesting(); // IN-TEST
ChipRenderParams chipRenderParamsForTesting = new ChipRenderParams();
chipRenderParamsForTesting.titleResourceId =
R.string.contextmenu_shop_image_with_google_lens;
chipRenderParamsForTesting.onClickCallback = () -> {};
mChipController.showChip(chipRenderParamsForTesting);
}
@VisibleForTesting
void simulateTranslateImageClassificationForTesting() {
// Don't need to initialize controller because that should be triggered by
// forcing feature flags.
mChipController.setFakeLensQueryResultForTesting(); // IN-TEST
ChipRenderParams chipRenderParamsForTesting = new ChipRenderParams();
chipRenderParamsForTesting.titleResourceId =
R.string.contextmenu_translate_image_with_google_lens;
chipRenderParamsForTesting.onClickCallback = () -> {};
mChipController.showChip(chipRenderParamsForTesting);
}
@VisibleForTesting
ChipRenderParams simulateImageClassificationForTesting() {
// Don't need to initialize controller because that should be triggered by
// forcing feature flags.
mChipController.setFakeLensQueryResultForTesting(); // IN-TEST
ChipRenderParams chipRenderParamsForTesting = new ChipRenderParams();
return chipRenderParamsForTesting;
}
// Public only to allow references from RevampedContextMenuUtils.java
public void clickChipForTesting() {
mChipController.clickChipForTesting(); // IN-TEST
}
// Public only to allow references from RevampedContextMenuUtils.java
public AnchoredPopupWindow getCurrentPopupWindowForTesting() {
// Don't need to initialize controller because that should be triggered by
// forcing feature flags.
return mChipController.getCurrentPopupWindowForTesting(); // IN-TEST
}
public void clickListItemForTesting(int id) {
mListView.performItemClick(null, -1, id);
}
@VisibleForTesting
ListItem getItem(int index) {
return (ListItem) mListView.getAdapter().getItem(index);
}
@VisibleForTesting
public int getCount() {
return mListView.getAdapter().getCount();
}
@VisibleForTesting
public ListItem findItem(int id) {
for (int i = 0; i < getCount(); i++) {
final ListItem item = getItem(i);
// If the item is a title/divider, its model does not have MENU_ID as key.
if (item.model.getAllSetProperties().contains(MENU_ID)
&& item.model.get(MENU_ID) == id) {
return item;
}
}
return null;
}
}