// 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.graphics.drawable.Drawable;
import androidx.appcompat.content.res.AppCompatResources;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.tasks.tab_management.TabUiMetricsHelper.TabListEditorActionMetricGroups;
import org.chromium.chrome.tab_ui.R;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/** Group action for the {@link TabListEditorMenu}. */
public class TabListEditorGroupAction extends TabListEditorAction {
private final TabGroupCreationDialogManager mTabGroupCreationDialogManager;
/**
* Create an action for grouping tabs.
*
* @param context for loading resources.
* @param tabGroupCreationDialogManager the manager for showing a dialog on group creation.
* @param showMode whether to show an action view.
* @param buttonType the type of the action view.
* @param iconPosition the position of the icon in the action view.
*/
public static TabListEditorAction createAction(
Context context,
TabGroupCreationDialogManager tabGroupCreationDialogManager,
@ShowMode int showMode,
@ButtonType int buttonType,
@IconPosition int iconPosition) {
Drawable drawable = AppCompatResources.getDrawable(context, R.drawable.ic_widgets);
return new TabListEditorGroupAction(
tabGroupCreationDialogManager, showMode, buttonType, iconPosition, drawable);
}
private TabListEditorGroupAction(
TabGroupCreationDialogManager tabGroupCreationDialogManager,
@ShowMode int showMode,
@ButtonType int buttonType,
@IconPosition int iconPosition,
Drawable drawable) {
super(
R.id.tab_list_editor_group_menu_item,
showMode,
buttonType,
iconPosition,
R.plurals.tab_selection_editor_group_tabs,
R.plurals.accessibility_tab_selection_editor_group_tabs,
drawable);
mTabGroupCreationDialogManager = tabGroupCreationDialogManager;
}
@Override
public void onSelectionStateChange(List<Integer> tabIds) {
int tabCount =
editorSupportsActionOnRelatedTabs()
? getTabCountIncludingRelatedTabs(getTabGroupModelFilter(), tabIds)
: tabIds.size();
boolean isEnabled = false;
int tabIdsSize = tabIds.size();
if (tabIdsSize == 1) {
TabGroupModelFilter filter = getTabGroupModelFilter();
Tab tab = filter.getTabModel().getTabById(tabIds.get(0));
isEnabled = tab != null && !filter.isTabInTabGroup(tab);
} else {
isEnabled = tabIdsSize > 1;
}
setEnabledAndItemCount(isEnabled, tabCount);
}
@Override
public boolean performAction(List<Tab> tabs) {
TabGroupModelFilter tabGroupModelFilter = getTabGroupModelFilter();
if (tabs.size() == 1) {
Tab tab = tabs.get(0);
if (tabGroupModelFilter.isTabInTabGroup(tab)) return true;
tabGroupModelFilter.createSingleTabGroup(tab, /* notify= */ true);
if (ChromeFeatureList.sTabGroupParityAndroid.isEnabled()
&& !TabGroupCreationDialogManager.shouldSkipGroupCreationDialog(
/* shouldShow= */ TabGroupCreationDialogManager
.shouldShowGroupCreationDialogViaSettingsSwitch())) {
mTabGroupCreationDialogManager.showDialog(tab.getRootId(), tabGroupModelFilter);
}
return true;
}
HashSet<Tab> selectedTabs = new HashSet<>(tabs);
Tab destinationTab =
getDestinationTab(
tabs,
tabGroupModelFilter,
editorSupportsActionOnRelatedTabs());
List<Tab> relatedTabs = tabGroupModelFilter.getRelatedTabList(destinationTab.getId());
selectedTabs.removeAll(relatedTabs);
// Sort tabs by index prevent visual bugs when undoing.
List<Tab> sortedTabs = new ArrayList<>(selectedTabs.size());
TabModel model = tabGroupModelFilter.getTabModel();
for (int i = 0; i < model.getCount(); i++) {
Tab tab = model.getTabAt(i);
if (!selectedTabs.contains(tab)) continue;
sortedTabs.add(tab);
}
List<Tab> tabsToMerge = new ArrayList<>();
tabsToMerge.addAll(sortedTabs);
tabsToMerge.add(destinationTab);
boolean willMergingCreateNewGroup =
tabGroupModelFilter.willMergingCreateNewGroup(tabsToMerge);
tabGroupModelFilter.mergeListOfTabsToGroup(sortedTabs, destinationTab, /* notify= */ true);
if (ChromeFeatureList.sTabGroupParityAndroid.isEnabled()
&& willMergingCreateNewGroup
&& !TabGroupCreationDialogManager.shouldSkipGroupCreationDialog(
/* shouldShow= */ TabGroupCreationDialogManager
.shouldShowGroupCreationDialogViaSettingsSwitch())) {
mTabGroupCreationDialogManager.showDialog(
destinationTab.getRootId(), tabGroupModelFilter);
}
TabUiMetricsHelper.recordSelectionEditorActionMetrics(
TabListEditorActionMetricGroups.GROUP);
return true;
}
@Override
public boolean shouldHideEditorAfterAction() {
return true;
}
/**
* Finds the tab to merge to. If at least one group is selected, merge all selected items to the
* group with the smallest group index. Otherwise, all selected items are merge to the tab with
* the largest tab index.
* @param tabs the list of all tabs to merge.
* @param filter the {@link TabGroupModelFilter} for managing groups.
* @param actionOnRelatedTabs whether to attempt to merge to groups.
* @return the tab to merge to.
*/
private Tab getDestinationTab(
List<Tab> tabs,
TabGroupModelFilter filter,
boolean actionOnRelatedTabs) {
TabModel model = filter.getTabModel();
int greatestTabIndex = TabModel.INVALID_TAB_INDEX;
int smallestGroupIndex = TabModel.INVALID_TAB_INDEX;
for (Tab tab : tabs) {
final int index = TabModelUtils.getTabIndexById(model, tab.getId());
greatestTabIndex = Math.max(index, greatestTabIndex);
if (actionOnRelatedTabs && filter.isTabInTabGroup(tab)) {
smallestGroupIndex =
(smallestGroupIndex == TabModel.INVALID_TAB_INDEX)
? index
: Math.min(index, smallestGroupIndex);
}
}
return model.getTabAt(
(smallestGroupIndex != TabModel.INVALID_TAB_INDEX)
? smallestGroupIndex
: greatestTabIndex);
}
}