blob: 06c4896276eff300be6795ac139372c142141a8a [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.tasks.tab_management;
import static org.chromium.chrome.browser.tasks.tab_management.TabSwitcherMediator.INITIAL_SCROLL_INDEX_OFFSET;
import android.content.Context;
import android.content.res.ColorStateList;
import android.support.v7.content.res.AppCompatResources;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import androidx.annotation.Nullable;
import org.chromium.base.Callback;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.EmptyTabModelObserver;
import org.chromium.chrome.browser.tabmodel.EmptyTabModelSelectorObserver;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.tabmodel.TabSelectionType;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.util.UrlConstants;
import org.chromium.chrome.browser.widget.ScrimView;
import org.chromium.chrome.tab_ui.R;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.List;
/**
* A mediator for the TabGridDialog component, responsible for communicating
* with the components' coordinator as well as managing the business logic
* for dialog show/hide.
*/
public class TabGridDialogMediator {
/**
* Defines an interface for a {@link TabGridDialogMediator} to control dialog.
*/
interface DialogController {
/**
* Handles a reset event originated from {@link TabGridDialogMediator} and {@link
* TabSwitcherMediator}.
*
* @param tabs List of Tabs to reset.
*/
void resetWithListOfTabs(@Nullable List<Tab> tabs);
/**
* Hide the TabGridDialog
* @param showAnimation Whether to show an animation when hiding the dialog.
*/
void hideDialog(boolean showAnimation);
/**
* @return Whether or not the TabGridDialog consumed the event.
*/
boolean handleBackPressed();
}
/**
* Defines an interface for a {@link TabGridDialogMediator} to get the {@link
* TabGridDialogParent.AnimationParams} in order to prepare show/hide animation.
*/
interface AnimationParamsProvider {
/**
* Provide a {@link TabGridDialogParent.AnimationParams} to setup the animation.
*
* @param tabId The id of the tab whose position is requested.
* @return A {@link TabGridDialogParent.AnimationParams} used to setup the animation.
*/
TabGridDialogParent.AnimationParams getAnimationParamsForTab(int tabId);
}
private final Context mContext;
private final PropertyModel mModel;
private final TabModelSelector mTabModelSelector;
private final TabModelSelectorObserver mTabModelSelectorObserver;
private final TabModelObserver mTabModelObserver;
private final TabCreatorManager mTabCreatorManager;
private final DialogController mDialogController;
private final TabSwitcherMediator.ResetHandler mTabSwitcherResetHandler;
private final AnimationParamsProvider mAnimationParamsProvider;
private final TabGroupTitleEditor mTabGroupTitleEditor;
private final DialogHandler mTabGridDialogHandler;
private final TabSelectionEditorCoordinator
.TabSelectionEditorController mTabSelectionEditorController;
private final String mComponentName;
private KeyboardVisibilityDelegate.KeyboardVisibilityListener mKeyboardVisibilityListener;
private int mCurrentTabId = Tab.INVALID_TAB_ID;
private boolean mIsUpdatingTitle;
private String mCurrentGroupModifiedTitle;
TabGridDialogMediator(Context context, DialogController dialogController, PropertyModel model,
TabModelSelector tabModelSelector, TabCreatorManager tabCreatorManager,
TabSwitcherMediator.ResetHandler tabSwitcherResetHandler,
AnimationParamsProvider animationParamsProvider,
TabSelectionEditorCoordinator.TabSelectionEditorController tabSelectionEditorController,
TabGroupTitleEditor tabGroupTitleEditor, String componentName) {
mContext = context;
mModel = model;
mTabModelSelector = tabModelSelector;
mTabCreatorManager = tabCreatorManager;
mDialogController = dialogController;
mTabSwitcherResetHandler = tabSwitcherResetHandler;
mAnimationParamsProvider = animationParamsProvider;
mTabGroupTitleEditor = tabGroupTitleEditor;
mTabGridDialogHandler = new DialogHandler();
mTabSelectionEditorController = tabSelectionEditorController;
mComponentName = componentName;
// Register for tab model.
mTabModelObserver = new EmptyTabModelObserver() {
@Override
public void didAddTab(Tab tab, @TabLaunchType int type) {
hideDialog(false);
}
@Override
public void tabClosureUndone(Tab tab) {
updateDialog();
updateGridTabSwitcher();
}
@Override
public void didSelectTab(Tab tab, int type, int lastId) {
if (type == TabSelectionType.FROM_USER) {
// Cancel the zooming into tab grid card animation.
hideDialog(false);
}
}
@Override
public void willCloseTab(Tab tab, boolean animate) {
List<Tab> relatedTabs = getRelatedTabs(tab.getId());
// If the group is empty, update the animation and hide the dialog.
if (relatedTabs.size() == 0) {
hideDialog(false);
return;
}
// If current tab is closed and tab group is not empty, hand over ID of the next
// tab in the group to mCurrentTabId.
if (tab.getId() == mCurrentTabId) {
mCurrentTabId = relatedTabs.get(0).getId();
}
updateDialog();
updateGridTabSwitcher();
}
};
mTabModelSelector.getTabModelFilterProvider().addTabModelFilterObserver(mTabModelObserver);
mTabModelSelectorObserver = new EmptyTabModelSelectorObserver() {
@Override
public void onTabModelSelected(TabModel newModel, TabModel oldModel) {
boolean isIncognito = newModel.isIncognito();
int dialogBackgroundResource = isIncognito
? R.drawable.tab_grid_dialog_background_incognito
: R.drawable.tab_grid_dialog_background;
ColorStateList tintList = isIncognito
? AppCompatResources.getColorStateList(mContext, R.color.tint_on_dark_bg)
: AppCompatResources.getColorStateList(
mContext, R.color.standard_mode_tint);
int ungroupBarBackgroundColorId = isIncognito
? R.color.tab_grid_dialog_background_color_incognito
: R.color.tab_grid_dialog_background_color;
int ungroupBarHoveredBackgroundColorId = isIncognito
? R.color.tab_grid_card_selected_color_incognito
: R.color.tab_grid_card_selected_color;
int ungroupBarTextAppearance = isIncognito
? R.style.TextAppearance_BlueTitle2Incognito
: R.style.TextAppearance_BlueTitle2;
mModel.set(TabGridPanelProperties.DIALOG_BACKGROUND_RESOUCE_ID,
dialogBackgroundResource);
mModel.set(TabGridPanelProperties.TINT, tintList);
mModel.set(TabGridPanelProperties.DIALOG_UNGROUP_BAR_BACKGROUND_COLOR_ID,
ungroupBarBackgroundColorId);
mModel.set(TabGridPanelProperties.DIALOG_UNGROUP_BAR_HOVERED_BACKGROUND_COLOR_ID,
ungroupBarHoveredBackgroundColorId);
mModel.set(TabGridPanelProperties.DIALOG_UNGROUP_BAR_TEXT_APPEARANCE,
ungroupBarTextAppearance);
}
};
mTabModelSelector.addObserver(mTabModelSelectorObserver);
assert mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter()
instanceof TabGroupModelFilter;
// Setup toolbar button click listeners.
setupToolbarClickHandlers();
// Setup dialog selection editor.
setupDialogSelectionEditor();
// Setup toolbar edit text.
setupToolbarEditText();
// Setup ScrimView observer.
setupScrimViewObserver();
}
void hideDialog(boolean showAnimation) {
if (!showAnimation) {
mModel.set(TabGridPanelProperties.ANIMATION_PARAMS, null);
} else {
if (mAnimationParamsProvider != null && mCurrentTabId != Tab.INVALID_TAB_ID) {
mModel.set(TabGridPanelProperties.ANIMATION_PARAMS,
mAnimationParamsProvider.getAnimationParamsForTab(mCurrentTabId));
}
}
mTabSelectionEditorController.hide();
saveCurrentGroupModifiedTitle();
mDialogController.resetWithListOfTabs(null);
}
void onReset(@Nullable List<Tab> tabs) {
if (tabs == null) {
mCurrentTabId = Tab.INVALID_TAB_ID;
} else {
TabModelFilter filter =
mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter();
mCurrentTabId = filter.getTabAt(filter.indexOf(tabs.get(0))).getId();
}
if (mCurrentTabId != Tab.INVALID_TAB_ID) {
if (mAnimationParamsProvider != null) {
TabGridDialogParent.AnimationParams params =
mAnimationParamsProvider.getAnimationParamsForTab(mCurrentTabId);
mModel.set(TabGridPanelProperties.ANIMATION_PARAMS, params);
}
updateDialog();
updateDialogScrollPosition();
mModel.set(TabGridPanelProperties.IS_DIALOG_VISIBLE, true);
} else {
mModel.set(TabGridPanelProperties.IS_DIALOG_VISIBLE, false);
}
}
/**
* Destroy any members that needs clean up.
*/
public void destroy() {
if (mTabModelObserver != null) {
mTabModelSelector.getTabModelFilterProvider().removeTabModelFilterObserver(
mTabModelObserver);
}
mTabModelSelector.removeObserver(mTabModelSelectorObserver);
KeyboardVisibilityDelegate.getInstance().removeKeyboardVisibilityListener(
mKeyboardVisibilityListener);
}
boolean isVisible() {
return mModel.get(TabGridPanelProperties.IS_DIALOG_VISIBLE);
}
private void updateGridTabSwitcher() {
if (!isVisible() || mTabSwitcherResetHandler == null) return;
mTabSwitcherResetHandler.resetWithTabList(
mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(), false,
false);
}
private void updateDialog() {
List<Tab> relatedTabs = getRelatedTabs(mCurrentTabId);
int tabsCount = relatedTabs.size();
if (tabsCount == 0) {
hideDialog(true);
return;
}
assert mTabGroupTitleEditor != null;
Tab currentTab = mTabModelSelector.getTabById(mCurrentTabId);
String storedTitle = mTabGroupTitleEditor.getTabGroupTitle(currentTab.getRootId());
if (storedTitle != null && relatedTabs.size() > 1) {
mModel.set(TabGridPanelProperties.HEADER_TITLE, storedTitle);
return;
}
mModel.set(TabGridPanelProperties.HEADER_TITLE,
mContext.getResources().getQuantityString(
R.plurals.bottom_tab_grid_title_placeholder, tabsCount, tabsCount));
}
private void updateDialogScrollPosition() {
// If current selected tab is not within this dialog, always scroll to the top.
if (mCurrentTabId != mTabModelSelector.getCurrentTabId()) {
mModel.set(TabGridPanelProperties.INITIAL_SCROLL_INDEX, 0);
return;
}
List<Tab> relatedTabs = getRelatedTabs(mCurrentTabId);
Tab currentTab = mTabModelSelector.getTabById(mCurrentTabId);
int initialPosition =
Math.max(relatedTabs.indexOf(currentTab) - INITIAL_SCROLL_INDEX_OFFSET, 0);
mModel.set(TabGridPanelProperties.INITIAL_SCROLL_INDEX, initialPosition);
}
private void setupToolbarClickHandlers() {
mModel.set(
TabGridPanelProperties.COLLAPSE_CLICK_LISTENER, getCollapseButtonClickListener());
mModel.set(TabGridPanelProperties.ADD_CLICK_LISTENER, getAddButtonClickListener());
mModel.set(TabGridPanelProperties.MENU_CLICK_LISTENER, getMenuButtonClickListener());
}
private void setupDialogSelectionEditor() {
TabSelectionEditorActionProvider actionProvider = new TabSelectionEditorActionProvider(
mTabModelSelector, mTabSelectionEditorController,
TabSelectionEditorActionProvider.TabSelectionEditorAction.UNGROUP);
String actionButtonText =
mContext.getString(R.string.tab_grid_dialog_selection_mode_remove);
mTabSelectionEditorController.configureToolbar(actionButtonText, actionProvider, 1, null);
}
private void setupToolbarEditText() {
mKeyboardVisibilityListener = isShowing -> {
mModel.set(TabGridPanelProperties.TITLE_CURSOR_VISIBILITY, isShowing);
mModel.set(TabGridPanelProperties.IS_TITLE_TEXT_FOCUSED, isShowing);
if (!isShowing) {
saveCurrentGroupModifiedTitle();
}
};
KeyboardVisibilityDelegate.getInstance().addKeyboardVisibilityListener(
mKeyboardVisibilityListener);
TextWatcher textWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
if (!mIsUpdatingTitle) return;
mCurrentGroupModifiedTitle = s.toString();
}
};
mModel.set(TabGridPanelProperties.TITLE_TEXT_WATCHER, textWatcher);
View.OnFocusChangeListener onFocusChangeListener =
(v, hasFocus) -> mIsUpdatingTitle = hasFocus;
mModel.set(TabGridPanelProperties.TITLE_TEXT_ON_FOCUS_LISTENER, onFocusChangeListener);
}
private void setupScrimViewObserver() {
ScrimView.ScrimObserver scrimObserver = new ScrimView.ScrimObserver() {
@Override
public void onScrimClick() {
hideDialog(true);
RecordUserAction.record("TabGridDialog.Exit");
}
@Override
public void onScrimVisibilityChanged(boolean visible) {}
};
mModel.set(TabGridPanelProperties.SCRIMVIEW_OBSERVER, scrimObserver);
}
private View.OnClickListener getCollapseButtonClickListener() {
return view -> {
hideDialog(true);
RecordUserAction.record("TabGridDialog.Exit");
};
}
private View.OnClickListener getAddButtonClickListener() {
return view -> {
// Get the current Tab first since hideDialog causes mCurrentTabId to be
// Tab.INVALID_TAB_ID.
Tab currentTab = mTabModelSelector.getTabById(mCurrentTabId);
hideDialog(false);
if (currentTab == null) {
mTabCreatorManager.getTabCreator(mTabModelSelector.isIncognitoSelected())
.launchNTP();
return;
}
List<Tab> relatedTabs = getRelatedTabs(currentTab.getId());
assert relatedTabs.size() > 0;
Tab parentTabToAttach = relatedTabs.get(relatedTabs.size() - 1);
mTabCreatorManager.getTabCreator(currentTab.isIncognito())
.createNewTab(new LoadUrlParams(UrlConstants.NTP_URL),
TabLaunchType.FROM_CHROME_UI, parentTabToAttach);
RecordUserAction.record("MobileNewTabOpened." + mComponentName);
};
}
private View.OnClickListener getMenuButtonClickListener() {
Callback<Integer> callback = result -> {
if (result == R.id.ungroup_tab) {
List<Tab> tabs = getRelatedTabs(mCurrentTabId);
mTabSelectionEditorController.show(tabs);
}
};
return TabGridDialogMenuCoordinator.getTabGridDialogMenuOnClickListener(callback);
}
private List<Tab> getRelatedTabs(int tabId) {
return mTabModelSelector.getTabModelFilterProvider()
.getCurrentTabModelFilter()
.getRelatedTabList(tabId);
}
private void saveCurrentGroupModifiedTitle() {
// When current group no longer exists, skip saving the title.
if (getRelatedTabs(mCurrentTabId).size() < 2) {
mCurrentGroupModifiedTitle = null;
}
if (mCurrentGroupModifiedTitle == null) {
return;
}
assert mTabGroupTitleEditor != null;
Tab currentTab = mTabModelSelector.getTabById(mCurrentTabId);
if (mCurrentGroupModifiedTitle.length() == 0) {
// When dialog title is empty, delete previously stored title and restore default title.
mTabGroupTitleEditor.deleteTabGroupTitle(currentTab.getRootId());
int tabsCount = getRelatedTabs(mCurrentTabId).size();
assert tabsCount >= 2;
String originalTitle = mContext.getResources().getQuantityString(
R.plurals.bottom_tab_grid_title_placeholder, tabsCount, tabsCount);
mModel.set(TabGridPanelProperties.HEADER_TITLE, originalTitle);
mTabGroupTitleEditor.updateTabGroupTitle(currentTab, originalTitle);
return;
}
mTabGroupTitleEditor.storeTabGroupTitle(currentTab.getRootId(), mCurrentGroupModifiedTitle);
mTabGroupTitleEditor.updateTabGroupTitle(currentTab, mCurrentGroupModifiedTitle);
mModel.set(TabGridPanelProperties.HEADER_TITLE, mCurrentGroupModifiedTitle);
mCurrentGroupModifiedTitle = null;
}
TabListMediator.TabGridDialogHandler getTabGridDialogHandler() {
return mTabGridDialogHandler;
}
/**
* A handler that handles TabGridDialog related changes originated from {@link TabListMediator}
* and {@link TabGridItemTouchHelperCallback}.
*/
class DialogHandler implements TabListMediator.TabGridDialogHandler {
@Override
public void updateUngroupBarStatus(@TabGridDialogParent.UngroupBarStatus int status) {
mModel.set(TabGridPanelProperties.UNGROUP_BAR_STATUS, status);
}
@Override
public void updateDialogContent(int tabId) {
mCurrentTabId = tabId;
updateDialog();
}
}
@VisibleForTesting
int getCurrentTabIdForTest() {
return mCurrentTabId;
}
@VisibleForTesting
void setCurrentTabIdForTest(int tabId) {
mCurrentTabId = tabId;
}
@VisibleForTesting
KeyboardVisibilityDelegate.KeyboardVisibilityListener
getKeyboardVisibilityListenerForTesting() {
return mKeyboardVisibilityListener;
}
@VisibleForTesting
boolean getIsUpdatingTitleForTesting() {
return mIsUpdatingTitle;
}
@VisibleForTesting
String getCurrentGroupModifiedTitleForTesting() {
return mCurrentGroupModifiedTitle;
}
}