// Copyright (c) 2024 The Brave Authors. All rights reserved. // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. // Based on Chromium code subject to the following license: // Copyright 2021 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "brave/browser/ui/commander/tab_command_source.h" #include #include #include #include #include #include #include "base/check.h" #include "base/functional/bind.h" #include "base/strings/utf_string_conversions.h" #include "brave/app/command_utils.h" #include "brave/browser/ui/commander/entity_match.h" #include "brave/browser/ui/commander/fuzzy_finder.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_window/public/global_browser_collection.h" #include "chrome/browser/ui/tabs/tab_enums.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/grit/brave_components_strings.h" #include "components/sessions/content/session_tab_helper.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 ItemForTitle(const std::u16string& title, FuzzyFinder& finder, std::vector& ranges) { double score = finder.Find(title, ranges); if (score > 0) { return std::make_unique(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. std::optional IneligibleGroupForSelected( TabStripModel* tab_strip_model) { std::optional excluded_group = std::nullopt; for (tabs::TabInterface* tab : tab_strip_model->selection_model().selected_tabs()) { if (auto group = tab->GetGroup(); 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 std::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, int index, int tab_session_id) { if (!browser.get()) { return false; } auto* tab_strip_model = browser->GetTabStripModel(); if (tab_strip_model->count() <= index) { return false; } content::WebContents* contents = tab_strip_model->GetWebContentsAt(index); DCHECK(contents); return sessions::SessionTabHelper::IdForTab(contents).id() == tab_session_id; } // Commands: bool HasUnpinnedTabs(const TabStripModel* model) { return model->IndexOfFirstNonPinnedTab() < model->count(); } bool HasPinnedTabs(const TabStripModel* model) { return model->IndexOfFirstNonPinnedTab() > 0; } bool CanMoveTabsToExistingWindow( const BrowserWindowInterface* browser_to_exclude) { bool has_found = false; GlobalBrowserCollection::GetInstance()->ForEach( [browser_to_exclude, &has_found](BrowserWindowInterface* browser) { has_found = browser != browser_to_exclude && browser->GetType() == BrowserWindowInterface::TYPE_NORMAL && browser->GetProfile() == browser_to_exclude->GetProfile(); return !has_found; }); return has_found; } void MoveTabsToExistingWindow(base::WeakPtr source, base::WeakPtr target) { if (!source.get() || !target.get()) { return; } const ui::ListSelectionModel::SelectedIndices& sel = source->GetTabStripModel() ->selection_model() .GetListSelectionModel() .selected_indices(); chrome::MoveTabsToExistingWindow(source.get(), target.get(), std::vector(sel.begin(), sel.end())); } void AddSelectedToNewGroup(BrowserWindowInterface* browser) { TabStripModel* model = browser->GetTabStripModel(); const ui::ListSelectionModel::SelectedIndices& sel = model->selection_model().GetListSelectionModel().selected_indices(); model->AddToNewGroup(std::vector(sel.begin(), sel.end())); } // Multiphase commands: void TogglePinTab(base::WeakPtr browser, int tab_index, int tab_session_id, bool pin) { if (!DoesTabAtIndexMatchSessionId(browser, tab_index, tab_session_id)) { return; } browser->GetTabStripModel()->SetTabPinned(tab_index, pin); } std::unique_ptr CreatePinTabItem(const TabMatch& match, BrowserWindowInterface* browser, bool pin) { auto item = match.ToCommandItem(); item->command = base::BindOnce(&TogglePinTab, browser->GetWeakPtr(), match.index, match.session_id, pin); return item; } CommandSource::CommandResults TogglePinTabCommandsForTabsMatching( BrowserWindowInterface* 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 CreateMoveTabsToWindowItem( BrowserWindowInterface* source, const WindowMatch& match) { auto item = match.ToCommandItem(); item->command = base::BindOnce(&MoveTabsToExistingWindow, source->GetWeakPtr(), match.browser->GetWeakPtr()); return item; } CommandSource::CommandResults MoveTabsToWindowCommandsForWindowsMatching( BrowserWindowInterface* 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 = base::UTF8ToUTF16(commands::GetCommandName(IDC_NEW_WINDOW)); std::unique_ptr item; if (input.empty()) { item = std::make_unique(new_window_title, .99, std::vector()); } else { FuzzyFinder finder(input); std::vector 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, tab_groups::TabGroupId group) { if (!browser.get()) { return; } const ui::ListSelectionModel::SelectedIndices& sel = browser->GetTabStripModel() ->selection_model() .GetListSelectionModel() .selected_indices(); browser->GetTabStripModel()->AddToExistingGroup( std::vector(sel.begin(), sel.end()), group); } CommandSource::CommandResults AddTabsToGroupCommandsForGroupsMatching( BrowserWindowInterface* browser, const std::u16string& input) { CommandSource::CommandResults results; TabStripModel* tab_strip_model = browser->GetTabStripModel(); // 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 item; if (input.empty()) { item = std::make_unique(new_group_title, .99, std::vector()); } else { FuzzyFinder finder(input); std::vector 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->GetWeakPtr(), 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, BrowserWindowInterface* browser) const { CommandSource::CommandResults results; FuzzyFinder finder(input); std::vector ranges; TabStripModel* tab_strip_model = browser->GetTabStripModel(); if (CanMoveTabsToExistingWindow(browser)) { auto text = l10n_util::GetStringUTF16(IDS_COMMANDER_MOVE_TABS_TO_WINDOW); if (auto item = ItemForTitle(text, finder, ranges)) { item->command = std::make_pair( text, base::BindRepeating(&MoveTabsToWindowCommandsForWindowsMatching, base::Unretained(browser))); results.push_back(std::move(item)); } } auto add_tab_to_existing_group = l10n_util::GetStringUTF16(IDS_COMMANDER_ADD_TABS_TO_EXISTING_GROUP); if (auto item = ItemForTitle(add_tab_to_existing_group, finder, ranges)) { item->command = std::make_pair( add_tab_to_existing_group, base::BindRepeating(&AddTabsToGroupCommandsForGroupsMatching, base::Unretained(browser))); results.push_back(std::move(item)); } if (HasUnpinnedTabs(tab_strip_model)) { auto text = l10n_util::GetStringUTF16(IDS_COMMANDER_PIN_TAB); if (auto item = ItemForTitle(text, finder, ranges)) { item->command = std::make_pair( text, base::BindRepeating(&TogglePinTabCommandsForTabsMatching, base::Unretained(browser), true)); results.push_back((std::move(item))); } } if (HasPinnedTabs(tab_strip_model)) { auto text = l10n_util::GetStringUTF16(IDS_COMMANDER_UNPIN_TAB); if (auto item = ItemForTitle(text, finder, ranges)) { item->command = std::make_pair( text, base::BindRepeating(&TogglePinTabCommandsForTabsMatching, base::Unretained(browser), false)); results.push_back((std::move(item))); } } return results; } } // namespace commander