blob: ebdc46846ec91eee7e6f0fef496c28de7a9bb3df [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/tabs/split_tab_menu_model.h"
#include <string>
#include "base/check.h"
#include "base/i18n/rtl.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/feedback/show_feedback_page.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/tabs/split_tab_util.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "components/tabs/public/split_tab_data.h"
#include "components/tabs/public/split_tab_id.h"
#include "components/tabs/public/split_tab_visual_data.h"
#include "components/tabs/public/tab_interface.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.h"
#include "ui/base/models/menu_separator_types.h"
#include "ui/menus/simple_menu_model.h"
namespace {
std::string GetMetricsSuffixForSource(
SplitTabMenuModel::MenuSource menu_source) {
// These strings are persisted to logs. Entries should not be changed.
switch (menu_source) {
case SplitTabMenuModel::MenuSource::kToolbarButton:
return "ToolbarButton";
case SplitTabMenuModel::MenuSource::kMiniToolbar:
return "MiniToolbar";
case SplitTabMenuModel::MenuSource::kTabContextMenu:
return "TabContextMenu";
}
}
int GetCommandIdInt(SplitTabMenuModel::CommandId command_id) {
// Start command IDs at 1701 to avoid conflicts with other submenus.
return ExistingBaseSubMenuModel::kMinSplitTabMenuModelCommandId +
static_cast<int>(command_id);
}
SplitTabMenuModel::CommandId GetCommandIdEnum(int command_id) {
return static_cast<SplitTabMenuModel::CommandId>(
command_id - ExistingBaseSubMenuModel::kMinSplitTabMenuModelCommandId);
}
Browser* GetBrowserWithTabStripModel(TabStripModel* tab_strip_model) {
for (Browser* browser : *BrowserList::GetInstance()) {
if (browser->tab_strip_model() == tab_strip_model) {
return browser;
}
}
return nullptr;
}
} // namespace
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SplitTabMenuModel,
kReversePositionMenuItem);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SplitTabMenuModel, kCloseMenuItem);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SplitTabMenuModel,
kCloseStartTabMenuItem);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SplitTabMenuModel, kCloseEndTabMenuItem);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SplitTabMenuModel, kExitSplitMenuItem);
SplitTabMenuModel::SplitTabMenuModel(TabStripModel* tab_strip_model,
MenuSource menu_source,
std::optional<int> split_tab_index)
: ui::SimpleMenuModel(this),
tab_strip_model_(tab_strip_model),
menu_source_(menu_source),
split_tab_index_(split_tab_index) {
AddItemWithStringIdAndIcon(
GetCommandIdInt(CommandId::kExitSplit), IDS_SPLIT_TAB_SEPARATE_VIEWS,
ui::ImageModel::FromVectorIcon(kOpenInFullIcon, ui::kColorMenuIcon,
ui::SimpleMenuModel::kDefaultIconSize));
AddSeparator(ui::MenuSeparatorType::NORMAL_SEPARATOR);
if (menu_source == MenuSource::kMiniToolbar) {
CHECK(split_tab_index.has_value());
AddItemWithStringIdAndIcon(
GetCommandIdInt(CommandId::kCloseSpecifiedTab), IDS_SPLIT_TAB_CLOSE,
ui::ImageModel::FromVectorIcon(vector_icons::kCloseChromeRefreshIcon,
ui::kColorMenuIcon,
ui::SimpleMenuModel::kDefaultIconSize));
SetElementIdentifierAt(
GetIndexOfCommandId(GetCommandIdInt(CommandId::kCloseSpecifiedTab))
.value(),
kCloseMenuItem);
} else if (menu_source == MenuSource::kTabContextMenu ||
menu_source == MenuSource::kToolbarButton) {
AddItem(GetCommandIdInt(CommandId::kCloseStartTab), std::u16string());
AddItem(GetCommandIdInt(CommandId::kCloseEndTab), std::u16string());
AddSeparator(ui::MenuSeparatorType::NORMAL_SEPARATOR);
SetElementIdentifierAt(
GetIndexOfCommandId(GetCommandIdInt(CommandId::kCloseStartTab)).value(),
kCloseStartTabMenuItem);
SetElementIdentifierAt(
GetIndexOfCommandId(GetCommandIdInt(CommandId::kCloseEndTab)).value(),
kCloseEndTabMenuItem);
} else {
NOTREACHED() << "Unknown close menu item option";
}
AddItem(GetCommandIdInt(CommandId::kReversePosition), std::u16string());
SetElementIdentifierAt(
GetIndexOfCommandId(GetCommandIdInt(CommandId::kReversePosition)).value(),
kReversePositionMenuItem);
SetElementIdentifierAt(
GetIndexOfCommandId(GetCommandIdInt(CommandId::kExitSplit)).value(),
kExitSplitMenuItem);
// Only render feedback in the toolbar button menu.
if (menu_source == MenuSource::kToolbarButton &&
tab_strip_model->profile()->GetPrefs()->GetBoolean(
prefs::kUserFeedbackAllowed)) {
AddSeparator(ui::MenuSeparatorType::NORMAL_SEPARATOR);
AddItemWithStringIdAndIcon(GetCommandIdInt(CommandId::kSendFeedback),
IDS_SPLIT_TAB_SEND_FEEDBACK,
ui::ImageModel::FromVectorIcon(kReportIcon));
}
}
SplitTabMenuModel::~SplitTabMenuModel() = default;
bool SplitTabMenuModel::IsItemForCommandIdDynamic(int command_id) const {
const CommandId id = GetCommandIdEnum(command_id);
return id == CommandId::kReversePosition || id == CommandId::kCloseStartTab ||
id == CommandId::kCloseEndTab;
}
std::u16string SplitTabMenuModel::GetLabelForCommandId(int command_id) const {
const CommandId id = GetCommandIdEnum(command_id);
if (id == CommandId::kReversePosition) {
return l10n_util::GetStringUTF16(IDS_SPLIT_TAB_REVERSE_VIEWS);
} else if (id == CommandId::kCloseStartTab) {
return l10n_util::GetStringUTF16(
GetSplitLayout() == split_tabs::SplitTabLayout::kVertical
? IDS_SPLIT_TAB_CLOSE_LEFT_VIEW
: IDS_SPLIT_TAB_CLOSE_TOP_VIEW);
} else if (id == CommandId::kCloseEndTab) {
return l10n_util::GetStringUTF16(
GetSplitLayout() == split_tabs::SplitTabLayout::kVertical
? IDS_SPLIT_TAB_CLOSE_RIGHT_VIEW
: IDS_SPLIT_TAB_CLOSE_BOTTOM_VIEW);
} else {
NOTREACHED() << "There are no other commands that are dynamic so this case "
"should not be reached.";
}
}
ui::ImageModel SplitTabMenuModel::GetIconForCommandId(int command_id) const {
const split_tabs::SplitTabActiveLocation active_split_tab_location =
split_tabs::GetLastActiveTabLocation(tab_strip_model_, GetSplitTabId());
const CommandId id = GetCommandIdEnum(command_id);
const gfx::VectorIcon* icon = nullptr;
if (id == CommandId::kReversePosition) {
icon = &GetReversePositionIcon(active_split_tab_location);
} else if (id == CommandId::kCloseStartTab) {
icon = GetSplitLayout() == split_tabs::SplitTabLayout::kVertical
? &kLeftPanelCloseIcon
: &kTopPanelCloseIcon;
} else if (id == CommandId::kCloseEndTab) {
icon = GetSplitLayout() == split_tabs::SplitTabLayout::kVertical
? &kRightPanelCloseIcon
: &kBottomPanelCloseIcon;
}
CHECK(icon);
return ui::ImageModel::FromVectorIcon(*icon, ui::kColorMenuIcon,
ui::SimpleMenuModel::kDefaultIconSize);
}
void SplitTabMenuModel::ExecuteCommand(int command_id, int event_flags) {
const split_tabs::SplitTabId split_id = GetSplitTabId();
split_tabs::SplitTabData* const split_tab_data =
tab_strip_model_->GetSplitData(split_id);
std::vector<tabs::TabInterface*> tabs_in_split = split_tab_data->ListTabs();
CHECK_EQ(tabs_in_split.size(), 2U);
CommandId split_command_id = GetCommandIdEnum(command_id);
switch (split_command_id) {
case CommandId::kReversePosition:
tab_strip_model_->ReverseTabsInSplit(split_id);
break;
case CommandId::kCloseSpecifiedTab:
CloseTabAtIndex(split_tab_index_.value());
break;
case CommandId::kCloseStartTab: {
int startIndex = base::i18n::IsRTL() ? 1 : 0;
CloseTabAtIndex(
tab_strip_model_->GetIndexOfTab(tabs_in_split[startIndex]));
break;
}
case CommandId::kCloseEndTab: {
int endIndex = base::i18n::IsRTL() ? 0 : 1;
CloseTabAtIndex(tab_strip_model_->GetIndexOfTab(tabs_in_split[endIndex]));
break;
}
case CommandId::kExitSplit:
tab_strip_model_->RemoveSplit(split_id);
break;
case CommandId::kSendFeedback:
SendFeedback();
break;
}
base::UmaHistogramEnumeration(
"Tabs.SplitViewMenu." + GetMetricsSuffixForSource(menu_source_),
split_command_id);
}
split_tabs::SplitTabId SplitTabMenuModel::GetSplitTabId() const {
tabs::TabInterface* const tab =
split_tab_index_.has_value()
? tab_strip_model_->GetTabAtIndex(split_tab_index_.value())
: tab_strip_model_->GetActiveTab();
CHECK(tab->IsSplit());
return tab->GetSplit().value();
}
const gfx::VectorIcon& SplitTabMenuModel::GetReversePositionIcon(
split_tabs::SplitTabActiveLocation active_split_tab_location) const {
switch (active_split_tab_location) {
case split_tabs::SplitTabActiveLocation::kStart:
return kSplitSceneRightIcon;
case split_tabs::SplitTabActiveLocation::kEnd:
return kSplitSceneLeftIcon;
case split_tabs::SplitTabActiveLocation::kTop:
return kSplitSceneDownIcon;
case split_tabs::SplitTabActiveLocation::kBottom:
return kSplitSceneUpIcon;
}
}
split_tabs::SplitTabLayout SplitTabMenuModel::GetSplitLayout() const {
split_tabs::SplitTabVisualData* const visual_data =
tab_strip_model_->GetSplitData(GetSplitTabId())->visual_data();
return visual_data->split_layout();
}
void SplitTabMenuModel::CloseTabAtIndex(int index) {
tab_strip_model_->CloseWebContentsAt(
index, TabCloseTypes::CLOSE_USER_GESTURE |
TabCloseTypes::CLOSE_CREATE_HISTORICAL_TAB);
}
void SplitTabMenuModel::SendFeedback() {
Browser* const browser = GetBrowserWithTabStripModel(tab_strip_model_);
CHECK(browser);
chrome::ShowFeedbackPage(browser, feedback::kFeedbackSourceSplitView, "", "",
"split_view", "");
}