blob: f7a1fed1c7b11a41ba1b5189b486e58420d1ec6b [file] [log] [blame]
// Copyright 2021 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.
#include "chrome/browser/ui/commander/tab_command_source.h"
#include <numeric>
#include <string>
#include "base/bind.h"
#include "base/containers/cxx20_erase.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/send_tab_to_self/send_tab_to_self_util.h"
#include "chrome/browser/ui/accelerator_utils.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/commander/entity_match.h"
#include "chrome/browser/ui/commander/fuzzy_finder.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/grit/generated_resources.h"
#include "components/sessions/content/session_tab_helper.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/list_selection_model.h"
namespace commander {
namespace {
// TODO(lgrey): It *might* make to pull this out later into a CommandSource
// method or a free function in some common place. Not committing yet.
std::unique_ptr<CommandItem> ItemForTitle(const std::u16string& title,
FuzzyFinder& finder,
std::vector<gfx::Range>* ranges) {
double score = finder.Find(title, ranges);
if (score > 0)
return std::make_unique<CommandItem>(title, score, *ranges);
return nullptr;
}
// Returns the tab group that the currently selected tabs can *not* be moved to.
// In practice, this is the tab group that *all* selected tabs belong to, if
// any. In the common special case of single selection, this will return that
// tab's group if it has one.
absl::optional<tab_groups::TabGroupId> IneligibleGroupForSelected(
TabStripModel* tab_strip_model) {
absl::optional<tab_groups::TabGroupId> excluded_group = absl::nullopt;
for (int index : tab_strip_model->selection_model().selected_indices()) {
auto group = tab_strip_model->GetTabGroupForTab(index);
if (group.has_value()) {
if (!excluded_group.has_value()) {
excluded_group = group;
} else if (group != excluded_group) {
// More than one group in the selection, so don't exclude anything.
return absl::nullopt;
}
}
}
return excluded_group;
}
// Returns true only if `browser` is alive, and the contents at `index` match
// `tab_session_id`.
bool DoesTabAtIndexMatchSessionId(base::WeakPtr<Browser> browser,
int index,
int tab_session_id) {
if (!browser.get())
return false;
if (browser->tab_strip_model()->count() <= index)
return false;
content::WebContents* contents =
browser->tab_strip_model()->GetWebContentsAt(index);
DCHECK(contents);
return sessions::SessionTabHelper::IdForTab(contents).id() == tab_session_id;
}
// Commands:
// TODO(lgrey): If this command ships, upstream these to TabStripModel
// (and get access to private methods for consistency).
bool CanCloseTabsToLeft(const TabStripModel* model) {
const ui::ListSelectionModel& selection = model->selection_model();
if (selection.empty())
return false;
int left_selected = *(selection.selected_indices().cbegin());
for (int i = 0; i < left_selected; ++i) {
if (!model->IsTabPinned(i))
return true;
}
return false;
}
void CloseTabsToLeft(Browser* browser) {
TabStripModel* model = browser->tab_strip_model();
const ui::ListSelectionModel& selection = model->selection_model();
if (selection.empty())
return;
int left_selected = *(selection.selected_indices().cbegin());
for (int i = left_selected - 1; i >= 0; --i) {
model->CloseWebContentsAt(i, TabStripModel::CLOSE_CREATE_HISTORICAL_TAB |
TabStripModel::CLOSE_USER_GESTURE);
}
}
bool HasUnpinnedTabs(const TabStripModel* model) {
return model->IndexOfFirstNonPinnedTab() < model->count();
}
bool HasPinnedTabs(const TabStripModel* model) {
return model->IndexOfFirstNonPinnedTab() > 0;
}
void CloseUnpinnedTabs(Browser* browser) {
TabStripModel* model = browser->tab_strip_model();
for (int i = model->count() - 1; i >= 0; --i) {
if (!model->IsTabPinned(i))
model->CloseWebContentsAt(i, TabStripModel::CLOSE_CREATE_HISTORICAL_TAB |
TabStripModel::CLOSE_USER_GESTURE);
}
}
bool CanMoveTabsToExistingWindow(const Browser* browser_to_exclude) {
const BrowserList* browser_list = BrowserList::GetInstance();
return std::any_of(
browser_list->begin(), browser_list->end(),
[browser_to_exclude](Browser* browser) {
return browser != browser_to_exclude && browser->is_type_normal() &&
browser->profile() == browser_to_exclude->profile();
});
}
void MoveTabsToExistingWindow(base::WeakPtr<Browser> source,
base::WeakPtr<Browser> target) {
if (!source.get() || !target.get())
return;
const ui::ListSelectionModel::SelectedIndices& sel =
source->tab_strip_model()->selection_model().selected_indices();
chrome::MoveTabsToExistingWindow(source.get(), target.get(),
std::vector<int>(sel.begin(), sel.end()));
}
bool CanAddAllToNewGroup(const TabStripModel* model) {
return model->group_model()->ListTabGroups().size() == 0;
}
void AddAllToNewGroup(Browser* browser) {
std::vector<int> indices(browser->tab_strip_model()->count());
std::iota(indices.begin(), indices.end(), 0);
browser->tab_strip_model()->AddToNewGroup(indices);
}
void AddSelectedToNewGroup(Browser* browser) {
TabStripModel* model = browser->tab_strip_model();
const ui::ListSelectionModel::SelectedIndices& sel =
model->selection_model().selected_indices();
model->AddToNewGroup(std::vector<int>(sel.begin(), sel.end()));
}
void MuteAllTabs(Browser* browser, bool exclude_active) {
TabStripModel* model = browser->tab_strip_model();
for (int i = 0; i < model->count(); ++i) {
if (exclude_active && i == model->active_index())
return;
content::WebContents* contents = model->GetWebContentsAt(i);
if (contents->IsCurrentlyAudible())
contents->SetAudioMuted(true);
}
}
// TODO(lgrey): Precalculate tab strip properties like "has audible tabs", "has
// pinned tabs" etc. in one iteration at search time.
bool HasAudibleTabs(const TabStripModel* model) {
for (int i = 0; i < model->count(); ++i) {
content::WebContents* contents = model->GetWebContentsAt(i);
if (contents->IsCurrentlyAudible())
return true;
}
return false;
}
bool HasMutedTabs(const TabStripModel* model) {
for (int i = 0; i < model->count(); ++i) {
content::WebContents* contents = model->GetWebContentsAt(i);
if (contents->IsAudioMuted())
return true;
}
return false;
}
void ScrollToTop(Browser* browser) {
browser->tab_strip_model()->GetActiveWebContents()->ScrollToTopOfDocument();
}
void ScrollToBottom(Browser* browser) {
browser->tab_strip_model()
->GetActiveWebContents()
->ScrollToBottomOfDocument();
}
// Multiphase commands:
void MuteUnmuteTab(base::WeakPtr<Browser> browser,
int tab_index,
int tab_session_id,
bool mute) {
if (!DoesTabAtIndexMatchSessionId(browser, tab_index, tab_session_id))
return;
browser->tab_strip_model()->GetWebContentsAt(tab_index)->SetAudioMuted(mute);
}
std::unique_ptr<CommandItem> CreateMuteUnmuteTabItem(const TabMatch& match,
Browser* browser,
bool mute) {
auto item = match.ToCommandItem();
item->command = base::BindOnce(&MuteUnmuteTab, browser->AsWeakPtr(),
match.index, match.session_id, mute);
return item;
}
CommandSource::CommandResults MuteUnmuteTabItemsForTabsMatching(
Browser* browser,
bool mute,
const std::u16string& input) {
CommandSource::CommandResults results;
TabSearchOptions options;
if (mute)
options.only_audible = true;
else
options.only_muted = true;
for (auto& match : TabsMatchingInput(browser, input, options)) {
results.push_back(CreateMuteUnmuteTabItem(match, browser, mute));
}
return results;
}
void TogglePinTab(base::WeakPtr<Browser> browser,
int tab_index,
int tab_session_id,
bool pin) {
if (!DoesTabAtIndexMatchSessionId(browser, tab_index, tab_session_id))
return;
browser->tab_strip_model()->SetTabPinned(tab_index, pin);
}
std::unique_ptr<CommandItem> CreatePinTabItem(const TabMatch& match,
Browser* browser,
bool pin) {
auto item = match.ToCommandItem();
item->command = base::BindOnce(&TogglePinTab, browser->AsWeakPtr(),
match.index, match.session_id, pin);
return item;
}
CommandSource::CommandResults TogglePinTabCommandsForTabsMatching(
Browser* browser,
bool pin,
const std::u16string& input) {
CommandSource::CommandResults results;
TabSearchOptions options;
if (pin)
options.only_unpinned = true;
else
options.only_pinned = true;
for (auto& match : TabsMatchingInput(browser, input, options)) {
results.push_back(CreatePinTabItem(match, browser, pin));
}
return results;
}
std::unique_ptr<CommandItem> CreateMoveTabsToWindowItem(
Browser* source,
const WindowMatch& match) {
auto item = match.ToCommandItem();
item->command = base::BindOnce(&MoveTabsToExistingWindow, source->AsWeakPtr(),
match.browser->AsWeakPtr());
return item;
}
CommandSource::CommandResults MoveTabsToWindowCommandsForWindowsMatching(
Browser* source,
const std::u16string& input) {
CommandSource::CommandResults results;
// Add "New Window", if appropriate. It should score highest with no input.
std::u16string new_window_title = l10n_util::GetStringUTF16(IDS_NEW_WINDOW);
base::Erase(new_window_title, '&');
std::unique_ptr<CommandItem> item;
if (input.empty()) {
item = std::make_unique<CommandItem>(new_window_title, .99,
std::vector<gfx::Range>());
} else {
FuzzyFinder finder(input);
std::vector<gfx::Range> ranges;
item = ItemForTitle(new_window_title, finder, &ranges);
}
if (item) {
item->entity_type = CommandItem::Entity::kWindow;
item->command = base::BindOnce(&chrome::MoveActiveTabToNewWindow,
base::Unretained(source));
results.push_back(std::move(item));
}
for (auto& match : WindowsMatchingInput(source, input))
results.push_back(CreateMoveTabsToWindowItem(source, match));
return results;
}
void AddTabsToGroup(base::WeakPtr<Browser> browser,
tab_groups::TabGroupId group) {
if (!browser.get())
return;
const ui::ListSelectionModel::SelectedIndices& sel =
browser->tab_strip_model()->selection_model().selected_indices();
browser->tab_strip_model()->AddToExistingGroup(
std::vector<int>(sel.begin(), sel.end()), group);
}
CommandSource::CommandResults AddTabsToGroupCommandsForGroupsMatching(
Browser* browser,
const std::u16string& input) {
CommandSource::CommandResults results;
TabStripModel* tab_strip_model = browser->tab_strip_model();
// Add "New Group", if appropriate. It should score highest with no input.
std::u16string new_group_title =
l10n_util::GetStringUTF16(IDS_TAB_CXMENU_SUBMENU_NEW_GROUP);
std::unique_ptr<CommandItem> item;
if (input.empty()) {
item = std::make_unique<CommandItem>(new_group_title, .99,
std::vector<gfx::Range>());
} else {
FuzzyFinder finder(input);
std::vector<gfx::Range> ranges;
item = ItemForTitle(new_group_title, finder, &ranges);
}
if (item) {
item->entity_type = CommandItem::Entity::kGroup;
item->command =
base::BindOnce(&AddSelectedToNewGroup, base::Unretained(browser));
results.push_back(std::move(item));
}
for (auto& match : GroupsMatchingInput(
browser, input, IneligibleGroupForSelected(tab_strip_model))) {
auto command_item = match.ToCommandItem();
command_item->command =
base::BindOnce(&AddTabsToGroup, browser->AsWeakPtr(), match.group);
results.push_back(std::move(command_item));
}
return results;
}
} // namespace
TabCommandSource::TabCommandSource() = default;
TabCommandSource::~TabCommandSource() = default;
CommandSource::CommandResults TabCommandSource::GetCommands(
const std::u16string& input,
Browser* browser) const {
CommandSource::CommandResults results;
FuzzyFinder finder(input);
std::vector<gfx::Range> ranges;
ui::AcceleratorProvider* provider =
chrome::AcceleratorProviderForBrowser(browser);
TabStripModel* tab_strip_model = browser->tab_strip_model();
// TODO(lgrey): Temporarily using hardcoded English titles instead of
// translated strings so we can experiment without adding translation load.
if (auto item = ItemForTitle(u"Close current tab", finder, &ranges)) {
item->command =
base::BindOnce(&chrome::CloseTab, base::Unretained(browser));
ui::Accelerator accelerator;
if (provider->GetAcceleratorForCommandId(IDC_CLOSE_TAB, &accelerator))
item->annotation = accelerator.GetShortcutText();
results.push_back(std::move(item));
}
if (chrome::CanCloseOtherTabs(browser)) {
if (auto item = ItemForTitle(u"Close other tabs", finder, &ranges)) {
item->command =
base::BindOnce(&chrome::CloseOtherTabs, base::Unretained(browser));
results.push_back(std::move(item));
}
}
if (chrome::CanCloseTabsToRight(browser)) {
if (auto item = ItemForTitle(u"Close tabs to the right", finder, &ranges)) {
item->command =
base::BindOnce(&chrome::CloseTabsToRight, base::Unretained(browser));
results.push_back(std::move(item));
}
}
if (CanCloseTabsToLeft(tab_strip_model)) {
if (auto item = ItemForTitle(u"Close tabs to the left", finder, &ranges)) {
item->command =
base::BindOnce(&CloseTabsToLeft, base::Unretained(browser));
results.push_back(std::move(item));
}
}
if (HasUnpinnedTabs(tab_strip_model)) {
if (auto item = ItemForTitle(u"Close unpinned tabs", finder, &ranges)) {
item->command =
base::BindOnce(&CloseUnpinnedTabs, base::Unretained(browser));
results.push_back(std::move(item));
}
}
if (chrome::CanMoveActiveTabToNewWindow(browser)) {
if (auto item =
ItemForTitle(l10n_util::GetStringUTF16(IDS_MOVE_TAB_TO_NEW_WINDOW),
finder, &ranges)) {
item->command = base::BindOnce(chrome::MoveActiveTabToNewWindow,
base::Unretained(browser));
results.push_back(std::move(item));
}
}
if (CanMoveTabsToExistingWindow(browser)) {
if (auto item = ItemForTitle(u"Move tabs to window...", finder, &ranges)) {
item->command = std::make_pair(
u"Move tabs to...",
base::BindRepeating(&MoveTabsToWindowCommandsForWindowsMatching,
base::Unretained(browser)));
results.push_back(std::move(item));
}
}
if (CanAddAllToNewGroup(tab_strip_model)) {
if (auto item =
ItemForTitle(u"Move all tabs to new group", finder, &ranges)) {
item->command =
base::BindOnce(&AddAllToNewGroup, base::Unretained(browser));
results.push_back(std::move(item));
}
}
if (!tab_strip_model->WillContextMenuGroup(tab_strip_model->active_index())) {
if (auto item = ItemForTitle(u"Ungroup tab", finder, &ranges)) {
item->command =
base::BindOnce(&chrome::GroupTab, base::Unretained(browser));
results.push_back(std::move(item));
}
}
if (auto item = ItemForTitle(u"Add tab to group...", finder, &ranges)) {
item->command = std::make_pair(
u"Add to group...",
base::BindRepeating(&AddTabsToGroupCommandsForGroupsMatching,
base::Unretained(browser)));
results.push_back(std::move(item));
}
if (auto item = ItemForTitle(u"Mute all tabs", finder, &ranges)) {
item->command =
base::BindOnce(&MuteAllTabs, base::Unretained(browser), false);
results.push_back(std::move(item));
}
if (auto item = ItemForTitle(u"Mute other tabs", finder, &ranges)) {
item->command =
base::BindOnce(&MuteAllTabs, base::Unretained(browser), false);
results.push_back(std::move(item));
}
if (HasAudibleTabs(tab_strip_model)) {
if (auto item = ItemForTitle(u"Mute tab...", finder, &ranges)) {
item->command =
std::make_pair(u"Mute tab...",
base::BindRepeating(&MuteUnmuteTabItemsForTabsMatching,
base::Unretained(browser), true));
results.push_back(std::move(item));
}
}
if (HasMutedTabs(tab_strip_model)) {
if (auto item = ItemForTitle(u"Unmute tab...", finder, &ranges)) {
item->command =
std::make_pair(u"Unmute tab...",
base::BindRepeating(&MuteUnmuteTabItemsForTabsMatching,
base::Unretained(browser), false));
results.push_back(std::move(item));
}
}
if (HasUnpinnedTabs(tab_strip_model)) {
if (auto item = ItemForTitle(u"Pin tab...", finder, &ranges)) {
item->command = std::make_pair(
u"Pin tab...",
base::BindRepeating(&TogglePinTabCommandsForTabsMatching,
base::Unretained(browser), true));
results.push_back((std::move(item)));
}
}
if (HasPinnedTabs(tab_strip_model)) {
if (auto item = ItemForTitle(u"Unpin tab...", finder, &ranges)) {
item->command = std::make_pair(
u"Unpin tab...",
base::BindRepeating(&TogglePinTabCommandsForTabsMatching,
base::Unretained(browser), false));
results.push_back((std::move(item)));
}
}
if (chrome::CanMoveActiveTabToReadLater(browser)) {
if (auto item = ItemForTitle(u"Add to Read Later", finder, &ranges)) {
item->command =
base::BindOnce(IgnoreResult(&chrome::MoveCurrentTabToReadLater),
base::Unretained(browser));
results.push_back(std::move(item));
}
}
if (auto item = ItemForTitle(u"Scroll to top", finder, &ranges)) {
item->command = base::BindOnce(&ScrollToTop, base::Unretained(browser));
results.push_back(std::move(item));
}
if (auto item = ItemForTitle(u"Scroll to bottom", finder, &ranges)) {
item->command = base::BindOnce(&ScrollToBottom, base::Unretained(browser));
results.push_back(std::move(item));
}
if (send_tab_to_self::ShouldDisplayEntryPoint(
tab_strip_model->GetActiveWebContents())) {
if (auto item = ItemForTitle(u"Send tab to self...", finder, &ranges)) {
item->command = base::BindOnce(&chrome::SendTabToSelfFromPageAction,
base::Unretained(browser));
results.push_back(std::move(item));
}
}
return results;
}
} // namespace commander