blob: b9d91654a77cf0f1a670f6e80642808ad7baec7f [file] [log] [blame]
// 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.
#include "chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_button.h"
#include <memory>
#include <string>
#include <vector>
#include "base/check.h"
#include "base/cxx20_to_address.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "cc/paint/paint_flags.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/favicon/favicon_utils.h"
#include "chrome/browser/ui/bookmarks/bookmark_utils_desktop.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_keyed_service.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_service_factory.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_group_theme.h"
#include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/bookmarks/bookmark_button_util.h"
#include "chrome/browser/ui/views/bookmarks/saved_tab_groups/saved_tab_group_drag_data.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h"
#include "chrome/grit/generated_resources.h"
#include "components/saved_tab_groups/saved_tab_group.h"
#include "components/tab_groups/tab_group_id.h"
#include "content/public/browser/page_navigator.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/dialog_model.h"
#include "ui/base/models/dialog_model_field.h"
#include "ui/base/models/dialog_model_menu_model_adapter.h"
#include "ui/base/models/image_model.h"
#include "ui/base/theme_provider.h"
#include "ui/base/ui_base_types.h"
#include "ui/color/color_id.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/label_button_border.h"
#include "ui/views/controls/button/menu_button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SavedTabGroupButton,
kDeleteGroupMenuItem);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(SavedTabGroupButton,
kMoveGroupToNewWindowMenuItem);
namespace {
// The max height of the button and the max width of a button with no title.
constexpr int kButtonSize = 24;
// The corner radius for the button.
constexpr float kButtonRadius = 4.0f;
// The amount of insets from the buttons border.
constexpr float kInsets = 5.0f;
// The width of the outline of the button when open in the Tab Strip.
constexpr float kBorderThickness = 1.0f;
// The radius for the circle that is displayed for buttons with no title.
constexpr float kCircleRadius = 7.0f;
} // namespace
SavedTabGroupButton::SavedTabGroupButton(
const SavedTabGroup& group,
base::RepeatingCallback<content::PageNavigator*()> page_navigator,
PressedCallback callback,
Browser* browser,
bool animations_enabled)
: MenuButton(std::move(callback), group.title()),
tab_group_color_id_(group.color()),
guid_(group.saved_guid()),
local_group_id_(group.local_group_id()),
tabs_(group.saved_tabs()),
browser_(*browser),
service_(
*SavedTabGroupServiceFactory::GetForProfile(browser_->profile())),
page_navigator_callback_(std::move(page_navigator)),
context_menu_controller_(
this,
base::BindRepeating(
&SavedTabGroupButton::CreateDialogModelForContextMenu,
base::Unretained(this)),
views::MenuRunner::CONTEXT_MENU | views::MenuRunner::IS_NESTED) {
SetAccessibilityProperties(
ax::mojom::Role::kButton, /*name=*/absl::nullopt,
/*description=*/absl::nullopt,
l10n_util::GetStringUTF16(
IDS_ACCNAME_SAVED_TAB_GROUP_BUTTON_ROLE_DESCRIPTION));
SetTextProperties(group);
SetID(VIEW_ID_BOOKMARK_BAR_ELEMENT);
SetProperty(views::kElementIdentifierKey, kSavedTabGroupButtonElementId);
SetMaxSize(gfx::Size(bookmark_button_util::kMaxButtonWidth, kButtonSize));
show_animation_ = std::make_unique<gfx::SlideAnimation>(this);
if (!animations_enabled) {
// For some reason during testing the events generated by animating throw
// off the test. So, don't animate while testing.
show_animation_->Reset(1);
} else {
show_animation_->Show();
}
ConfigureInkDropForToolbar(this);
SetImageLabelSpacing(ChromeLayoutProvider::Get()->GetDistanceMetric(
ChromeDistanceMetric::DISTANCE_RELATED_LABEL_HORIZONTAL_LIST));
views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(0),
kButtonRadius);
set_drag_controller(this);
}
SavedTabGroupButton::~SavedTabGroupButton() = default;
void SavedTabGroupButton::UpdateButtonData(const SavedTabGroup& group) {
SetTextProperties(group);
tab_group_color_id_ = group.color();
local_group_id_ = group.local_group_id();
guid_ = group.saved_guid();
tabs_.clear();
tabs_ = group.saved_tabs();
UpdateButtonLayout();
}
std::u16string SavedTabGroupButton::GetTooltipText(const gfx::Point& p) const {
return label()->GetPreferredSize().width() > label()->size().width()
? GetText()
: std::u16string();
}
void SavedTabGroupButton::GetAccessibleNodeData(ui::AXNodeData* node_data) {
views::MenuButton::GetAccessibleNodeData(node_data);
node_data->SetNameChecked(GetAccessibleNameForButton());
node_data->role = ax::mojom::Role::kButton;
}
void SavedTabGroupButton::PaintButtonContents(gfx::Canvas* canvas) {
if (!GetText().empty()) {
return;
}
// When the title is empty, we draw a circle similar to the tab group
// header when there is no title.
const ui::ColorProvider* const cp = GetColorProvider();
SkColor text_and_outline_color =
cp->GetColor(GetTabGroupDialogColorId(tab_group_color_id_));
// Draw circle.
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(text_and_outline_color);
const gfx::PointF center_point_f = gfx::PointF(width() / 2, height() / 2);
canvas->DrawCircle(center_point_f, kCircleRadius, flags);
}
std::u16string SavedTabGroupButton::GetAccessibleNameForButton() {
const std::u16string& opened_state =
local_group_id_.has_value()
? l10n_util::GetStringUTF16(IDS_SAVED_GROUP_AX_LABEL_OPENED)
: l10n_util::GetStringUTF16(IDS_SAVED_GROUP_AX_LABEL_CLOSED);
const std::u16string saved_group_acessible_name =
GetText().empty()
? l10n_util::GetStringFUTF16(
IDS_GROUP_AX_LABEL_UNNAMED_SAVED_GROUP_FORMAT, opened_state)
: l10n_util::GetStringFUTF16(
IDS_GROUP_AX_LABEL_NAMED_SAVED_GROUP_FORMAT, GetText(),
opened_state);
return saved_group_acessible_name;
}
void SavedTabGroupButton::SetTextProperties(const SavedTabGroup& group) {
SetAccessibleName(GetAccessibleNameForButton());
SetTooltipText(group.title());
SetText(group.title());
}
void SavedTabGroupButton::UpdateButtonLayout() {
if (GetText().empty()) {
// When the text is empty force the button to have square dimensions.
SetPreferredSize(gfx::Size(kButtonSize, kButtonSize));
} else {
SetPreferredSize(CalculatePreferredSize());
}
// Relies on logic in theme_helper.cc to determine dark/light palette.
ui::ColorId text_and_outline_color =
GetTabGroupDialogColorId(tab_group_color_id_);
ui::ColorId background_color =
GetTabGroupBookmarkColorId(tab_group_color_id_);
SetEnabledTextColorIds(GetTabGroupDialogColorId(tab_group_color_id_));
SetBackground(views::CreateThemedRoundedRectBackground(background_color,
kButtonRadius));
// Only draw a border if the group is open in the tab strip.
if (!local_group_id_.has_value()) {
SetBorder(views::CreateEmptyBorder(gfx::Insets(kInsets)));
} else {
std::unique_ptr<views::Border> border =
views::CreateThemedRoundedRectBorder(kBorderThickness, kButtonRadius,
text_and_outline_color);
SetBorder(
views::CreatePaddedBorder(std::move(border), gfx::Insets(kInsets)));
}
}
std::unique_ptr<views::LabelButtonBorder>
SavedTabGroupButton::CreateDefaultBorder() const {
auto border = std::make_unique<views::LabelButtonBorder>();
border->set_insets(ChromeLayoutProvider::Get()->GetInsetsMetric(
INSETS_BOOKMARKS_BAR_BUTTON));
return border;
}
void SavedTabGroupButton::OnThemeChanged() {
views::MenuButton::OnThemeChanged();
UpdateButtonLayout();
}
void SavedTabGroupButton::WriteDragDataForView(View* sender,
const gfx::Point& press_pt,
ui::OSExchangeData* data) {
SavedTabGroupButton* const button =
views::AsViewClass<SavedTabGroupButton>(sender);
CHECK(button);
CHECK(button == this);
// Write the image and MIME type to the OSExchangeData.
SavedTabGroupDragData::WriteToOSExchangeData(this, press_pt,
GetThemeProvider(), data);
}
int SavedTabGroupButton::GetDragOperationsForView(View* sender,
const gfx::Point& p) {
// This may need to become more complicated
return ui::DragDropTypes::DRAG_MOVE;
}
bool SavedTabGroupButton::CanStartDragForView(View* sender,
const gfx::Point& press_pt,
const gfx::Point& p) {
// Check if we have not moved enough horizontally but we have moved downward
// vertically - downward drag.
gfx::Vector2d move_offset = p - press_pt;
return View::ExceededDragThreshold(move_offset);
}
void SavedTabGroupButton::TabMenuItemPressed(const GURL& url, int event_flags) {
CHECK(page_navigator_callback_);
content::OpenURLParams params(url, content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_AUTO_BOOKMARK,
/*is_renderer_initiated=*/false,
/*started_from_context_menu=*/true);
page_navigator_callback_.Run()->OpenURL(params);
}
void SavedTabGroupButton::MoveGroupToNewWindowPressed(int event_flags) {
Browser* const browser_with_local_group_id =
local_group_id_.has_value()
? SavedTabGroupUtils::GetBrowserWithTabGroupId(
local_group_id_.value())
: base::to_address(browser_);
if (!local_group_id_.has_value()) {
// Open the group in the browser the button was pressed.
service_->OpenSavedTabGroupInBrowser(browser_with_local_group_id, guid_);
}
// Move the open group to a new browser window.
const SavedTabGroup* group = service_->model()->Get(guid_);
browser_with_local_group_id->tab_strip_model()
->delegate()
->MoveGroupToNewWindow(group->local_group_id().value());
}
void SavedTabGroupButton::DeleteGroupPressed(int event_flags) {
if (local_group_id_.has_value()) {
const Browser* const browser_with_local_group_id =
SavedTabGroupUtils::GetBrowserWithTabGroupId(local_group_id_.value());
// Keep the opened tab group in the tabstrip but remove the SavedTabGroup
// data from the model.
TabGroup* tab_group = browser_with_local_group_id->tab_strip_model()
->group_model()
->GetTabGroup(local_group_id_.value());
service_->UnsaveGroup(local_group_id_.value());
// Notify observers to update the tab group header.
// TODO(dljames): Find a way to move this into
// SavedTabGroupKeyedService::DisconnectLocalTabGroup. The goal is to
// abstract this logic from the button in case we need to do similar
// functionality elsewhere in the future. Ensure this change works when
// dragging a Saved group out of the window.
tab_group->SetVisualData(*tab_group->visual_data());
} else {
// Remove the SavedTabGroup from the model. No need to worry about updating
// tabstrip, since this group is not open.
service_->model()->Remove(guid_);
}
}
std::unique_ptr<ui::DialogModel>
SavedTabGroupButton::CreateDialogModelForContextMenu() {
ui::DialogModel::Builder dialog_model = ui::DialogModel::Builder();
const std::u16string move_or_open_group_text =
local_group_id_.has_value()
? l10n_util::GetStringUTF16(
IDS_TAB_GROUP_HEADER_CXMENU_MOVE_GROUP_TO_NEW_WINDOW)
: l10n_util::GetStringUTF16(
IDS_TAB_GROUP_HEADER_CXMENU_OPEN_GROUP_IN_NEW_WINDOW);
dialog_model
.AddMenuItem(
ui::ImageModel::FromVectorIcon(kMoveGroupToNewWindowIcon),
move_or_open_group_text,
base::BindRepeating(&SavedTabGroupButton::MoveGroupToNewWindowPressed,
base::Unretained(this)),
ui::DialogModelMenuItem::Params().SetId(
kMoveGroupToNewWindowMenuItem))
.AddMenuItem(
ui::ImageModel::FromVectorIcon(kCloseGroupIcon),
l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_DELETE_GROUP),
base::BindRepeating(&SavedTabGroupButton::DeleteGroupPressed,
base::Unretained(this)),
ui::DialogModelMenuItem::Params().SetId(kDeleteGroupMenuItem))
.AddSeparator();
for (const SavedTabGroupTab& tab : tabs_) {
const ui::ImageModel& image =
tab.favicon().has_value()
? ui::ImageModel::FromImage(tab.favicon().value())
: favicon::GetDefaultFaviconModel(
GetTabGroupBookmarkColorId(tab_group_color_id_));
const std::u16string title =
tab.title().empty() ? base::UTF8ToUTF16(tab.url().spec()) : tab.title();
dialog_model.AddMenuItem(
image, title,
base::BindRepeating(&SavedTabGroupButton::TabMenuItemPressed,
base::Unretained(this), tab.url()));
}
return dialog_model.Build();
}
BEGIN_METADATA(SavedTabGroupButton, MenuButton)
END_METADATA