| // 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 |