blob: 89edc1e0d36947651c054aebc082ab7862d66246 [file] [log] [blame]
// Copyright 2019 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 <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/containers/adapters.h"
#include "base/containers/flat_map.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/tabs/tab_group.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/bubble_menu_item_factory.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/hover_button.h"
#include "chrome/browser/ui/views/tabs/color_picker_view.h"
#include "chrome/browser/ui/views/tabs/tab_group_editor_bubble_view.h"
#include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h"
#include "chrome/grit/generated_resources.h"
#include "components/tab_groups/tab_group_color.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/dialog_model_field.h"
#include "ui/base/models/image_model.h"
#include "ui/base/pointer/touch_ui_controller.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/range/range.h"
#include "ui/gfx/text_constants.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/border.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/bubble/bubble_dialog_model_host.h"
#include "ui/views/bubble/bubble_frame_view.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/button/toggle_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
namespace {
std::unique_ptr<views::LabelButton> CreateMenuItem(
int button_id,
const std::u16string& name,
views::Button::PressedCallback callback,
const gfx::VectorIcon* icon = nullptr) {
const auto* layout_provider = ChromeLayoutProvider::Get();
const int horizontal_spacing = layout_provider->GetDistanceMetric(
views::DISTANCE_RELATED_CONTROL_HORIZONTAL);
const int vertical_spacing = layout_provider->GetDistanceMetric(
views::DISTANCE_RELATED_CONTROL_VERTICAL);
const gfx::Insets control_insets =
ui::TouchUiController::Get()->touch_ui()
? gfx::Insets(5 * vertical_spacing / 4, horizontal_spacing)
: gfx::Insets(vertical_spacing, horizontal_spacing);
auto button = CreateBubbleMenuItem(button_id, name, callback, icon);
button->SetBorder(views::CreateEmptyBorder(control_insets));
return button;
}
class MenuItemFactory : public views::BubbleDialogModelHost::CustomViewFactory {
public:
MenuItemFactory(const std::u16string& name,
views::Button::PressedCallback callback,
const gfx::VectorIcon* icon = nullptr)
: name_(std::move(name)), callback_(std::move(callback)), icon_(icon) {}
~MenuItemFactory() override = default;
// views::BubbleDialogModelHost::CustomViewFactory:
std::unique_ptr<views::View> CreateView() override {
// TODO(pbos): See if dialog_model()->host()->Close(); can be handled by the
// menu item itself (after calling callback_). All menu-item actions close
// the dialog. We should be able to chain some calls if we have access to
// DialogModelDelegate here.
return CreateMenuItem(-1, name_, callback_, icon_);
}
views::BubbleDialogModelHost::FieldType GetFieldType() const override {
return views::BubbleDialogModelHost::FieldType::kMenuItem;
}
private:
const std::u16string name_;
const views::Button::PressedCallback callback_;
const raw_ptr<const gfx::VectorIcon> icon_;
};
class TabGroupEditorBubbleDelegate : public ui::DialogModelDelegate {
public:
TabGroupEditorBubbleDelegate(const Browser* browser,
const tab_groups::TabGroupId& group)
: browser_(browser), group_(group) {}
TabGroupEditorBubbleDelegate(const TabGroupEditorBubbleDelegate&) = delete;
TabGroupEditorBubbleDelegate& operator=(const TabGroupEditorBubbleDelegate&) =
delete;
~TabGroupEditorBubbleDelegate() override = default;
void NewTabInGroupPressed() {
base::RecordAction(
base::UserMetricsAction("TabGroups_TabGroupBubble_NewTabInGroup"));
TabStripModel* const model = browser_->tab_strip_model();
const auto tabs = model->group_model()->GetTabGroup(group_)->ListTabs();
model->delegate()->AddTabAt(GURL(), tabs.end(), true, group_);
// Close the widget to allow users to continue their work in their newly
// created tab.
dialog_model()->host()->Close();
}
void UngroupPressed(TabGroupHeader* header_view) {
base::RecordAction(
base::UserMetricsAction("TabGroups_TabGroupBubble_Ungroup"));
if (header_view) {
// TODO(pbos): See if this can be managed outside this dialog to prevent
// upcasting to BubbleDialogModelHost.
header_view->RemoveObserverFromWidget(
static_cast<views::BubbleDialogModelHost*>(dialog_model()->host())
->GetWidget());
}
TabStripModel* const model = browser_->tab_strip_model();
const gfx::Range tab_range =
model->group_model()->GetTabGroup(group_)->ListTabs();
std::vector<int> tabs;
tabs.reserve(tab_range.length());
for (auto i = tab_range.start(); i < tab_range.end(); ++i)
tabs.push_back(i);
model->RemoveFromGroup(tabs);
// Close the widget because it is no longer applicable.
dialog_model()->host()->Close();
}
void CloseGroupPressed() {
base::RecordAction(
base::UserMetricsAction("TabGroups_TabGroupBubble_CloseGroup"));
browser_->tab_strip_model()->CloseAllTabsInGroup(group_);
// Close the widget because it is no longer applicable.
dialog_model()->host()->Close();
}
void MoveGroupToNewWindowPressed() {
browser_->tab_strip_model()->delegate()->MoveGroupToNewWindow(group_);
dialog_model()->host()->Close();
}
void SendFeedbackPressed() {
base::RecordAction(
base::UserMetricsAction("TabGroups_TabGroupBubble_SendFeedback"));
chrome::ShowFeedbackPage(
browser_, chrome::FeedbackSource::kFeedbackSourceDesktopTabGroups,
/*description_template=*/std::string(),
/*description_placeholder_text=*/std::string(),
/*category_tag=*/std::string(),
/*extra_diagnostics=*/std::string());
dialog_model()->host()->Close();
}
private:
const raw_ptr<const Browser> browser_;
const tab_groups::TabGroupId group_;
};
} // namespace
// static
views::Widget* TabGroupEditorBubbleView::Show(
const Browser* browser,
const tab_groups::TabGroupId& group,
TabGroupHeader* header_view,
absl::optional<gfx::Rect> anchor_rect,
views::View* anchor_view,
bool stop_context_menu_propagation) {
// TODO(pbos): Clean this duplicate implementation up. This is only here while
// development of a DialogModel version of this bubble is in progress. This is
// also only checked in so that development on DialogModel and
// BubbleDialogModelHost can happen in chunks and be checked in instead of
// landed as a gargantuan change.
static constexpr bool kUseDialogModel = false;
if (kUseDialogModel) {
auto bubble_delegate_unique =
std::make_unique<TabGroupEditorBubbleDelegate>(browser, group);
TabGroupEditorBubbleDelegate* const bubble_delegate =
bubble_delegate_unique.get();
ui::DialogModel::Builder dialog_builder(std::move(bubble_delegate_unique));
// TODO(pbos): This still does not include the title field, divider or color
// picker.
dialog_builder.OverrideShowCloseButton(false)
.AddCustomField(
std::make_unique<MenuItemFactory>(
l10n_util::GetStringUTF16(
IDS_TAB_GROUP_HEADER_CXMENU_NEW_TAB_IN_GROUP),
base::BindRepeating(
&TabGroupEditorBubbleDelegate::NewTabInGroupPressed,
base::Unretained(bubble_delegate)),
&kNewTabInGroupIcon),
TAB_GROUP_HEADER_CXMENU_NEW_TAB_IN_GROUP)
.AddCustomField(
std::make_unique<MenuItemFactory>(
l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_UNGROUP),
base::BindRepeating(
&TabGroupEditorBubbleDelegate::UngroupPressed,
base::Unretained(bubble_delegate), header_view),
&kUngroupIcon),
TAB_GROUP_HEADER_CXMENU_UNGROUP)
.AddCustomField(
std::make_unique<MenuItemFactory>(
l10n_util::GetStringUTF16(
IDS_TAB_GROUP_HEADER_CXMENU_CLOSE_GROUP),
base::BindRepeating(
&TabGroupEditorBubbleDelegate::CloseGroupPressed,
base::Unretained(bubble_delegate)),
&kCloseGroupIcon),
TAB_GROUP_HEADER_CXMENU_CLOSE_GROUP)
.AddCustomField(
std::make_unique<MenuItemFactory>(
l10n_util::GetStringUTF16(
IDS_TAB_GROUP_HEADER_CXMENU_MOVE_GROUP_TO_NEW_WINDOW),
base::BindRepeating(
&TabGroupEditorBubbleDelegate::MoveGroupToNewWindowPressed,
base::Unretained(bubble_delegate)),
&kMoveGroupToNewWindowIcon),
TAB_GROUP_HEADER_CXMENU_MOVE_GROUP_TO_NEW_WINDOW);
// TODO(pbos): Add enabling/disabling of
// TAB_GROUP_HEADER_CXMENU_MOVE_GROUP_TO_NEW_WINDOW item.
std::unique_ptr<ui::DialogModel> dialog_model = dialog_builder.Build();
// If |header_view| is not null, use |header_view| as the |anchor_view|.
auto bubble = std::make_unique<views::BubbleDialogModelHost>(
std::move(dialog_model), header_view ? header_view : anchor_view,
views::BubbleBorder::TOP_LEFT);
if (anchor_rect)
bubble->SetAnchorRect(*anchor_rect);
views::BubbleDialogModelHost* const bubble_ptr = bubble.get();
views::Widget* const widget =
views::BubbleDialogDelegate::CreateBubble(std::move(bubble));
bubble_ptr->set_adjust_if_offscreen(true);
bubble_ptr->GetBubbleFrameView()->SetPreferredArrowAdjustment(
views::BubbleFrameView::PreferredArrowAdjustment::kOffset);
widget->Show();
return widget;
}
// If |header_view| is not null, use |header_view| as the |anchor_view|.
TabGroupEditorBubbleView* tab_group_editor_bubble_view =
new TabGroupEditorBubbleView(
browser, group, header_view ? header_view : anchor_view, anchor_rect,
header_view, stop_context_menu_propagation);
views::Widget* const widget =
BubbleDialogDelegateView::CreateBubble(tab_group_editor_bubble_view);
tab_group_editor_bubble_view->set_adjust_if_offscreen(true);
tab_group_editor_bubble_view->GetBubbleFrameView()
->SetPreferredArrowAdjustment(
views::BubbleFrameView::PreferredArrowAdjustment::kOffset);
tab_group_editor_bubble_view->SizeToContents();
widget->Show();
return widget;
}
views::View* TabGroupEditorBubbleView::GetInitiallyFocusedView() {
return title_field_;
}
gfx::Rect TabGroupEditorBubbleView::GetAnchorRect() const {
// We want to avoid calling BubbleDialogDelegateView::GetAnchorRect() if
// |anchor_rect_| has been set. This is because the default behavior uses the
// anchor view's bounds and also updates |anchor_rect_| to the views bounds.
// It does this so that the bubble does not jump when the anchoring view is
// deleted.
if (use_set_anchor_rect_)
return anchor_rect().value();
return BubbleDialogDelegateView::GetAnchorRect();
}
void TabGroupEditorBubbleView::AddedToWidget() {
if (!move_menu_item_->GetEnabled()) {
const SkColor disabled_color = move_menu_item_->GetCurrentTextColor();
move_menu_item_->SetImageModel(
views::Button::STATE_DISABLED,
ui::ImageModel::FromVectorIcon(kMoveGroupToNewWindowIcon,
disabled_color));
}
}
TabGroupEditorBubbleView::TabGroupEditorBubbleView(
const Browser* browser,
const tab_groups::TabGroupId& group,
views::View* anchor_view,
absl::optional<gfx::Rect> anchor_rect,
TabGroupHeader* header_view,
bool stop_context_menu_propagation)
: browser_(browser),
group_(group),
title_field_controller_(this),
use_set_anchor_rect_(anchor_rect) {
// |anchor_view| should always be defined as it will be used to source the
// |anchor_widget_|.
DCHECK(anchor_view);
SetAnchorView(anchor_view);
if (anchor_rect)
SetAnchorRect(anchor_rect.value());
set_margins(gfx::Insets());
SetButtons(ui::DIALOG_BUTTON_NONE);
SetModalType(ui::MODAL_TYPE_NONE);
TabStripModel* const tab_strip_model = browser_->tab_strip_model();
const std::u16string title = tab_strip_model->group_model()
->GetTabGroup(group_)
->visual_data()
->title();
title_at_opening_ = title;
SetCloseCallback(base::BindOnce(&TabGroupEditorBubbleView::OnBubbleClose,
base::Unretained(this)));
// Create view hierarchy.
title_field_ =
AddChildView(std::make_unique<TitleField>(stop_context_menu_propagation));
title_field_->SetText(title);
title_field_->SetAccessibleName(u"Group title");
title_field_->SetPlaceholderText(
l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_BUBBLE_TITLE_PLACEHOLDER));
title_field_->set_controller(&title_field_controller_);
title_field_->SetProperty(views::kElementIdentifierKey,
kEditorBubbleIdentifier);
const tab_groups::TabGroupColorId initial_color_id = InitColorSet();
color_selector_ = AddChildView(std::make_unique<ColorPickerView>(
this, colors_, initial_color_id,
base::BindRepeating(&TabGroupEditorBubbleView::UpdateGroup,
base::Unretained(this))));
auto* const separator = AddChildView(std::make_unique<views::Separator>());
views::ImageView* save_group_icon = nullptr;
views::View* save_group_line_container = nullptr;
views::Label* save_group_label = nullptr;
if (base::FeatureList::IsEnabled(features::kTabGroupsSave)) {
save_group_line_container = AddChildView(std::make_unique<views::View>());
// The save_group_icon is put in differently than the rest because it
// utilizes a different view (view::Label) that does not have an option to
// take in an image like the other line items do.
save_group_icon = save_group_line_container->AddChildView(
std::make_unique<views::ImageView>(
ui::ImageModel::FromVectorIcon(kSaveGroupIcon)));
save_group_label =
save_group_line_container->AddChildView(std::make_unique<views::Label>(
l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_SAVE_GROUP)));
save_group_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
save_group_toggle_ = save_group_line_container->AddChildView(
std::make_unique<views::ToggleButton>(
base::BindRepeating(&TabGroupEditorBubbleView::OnSaveTogglePressed,
base::Unretained(this))));
save_group_toggle_->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_SAVE_GROUP));
bool is_saved =
tab_strip_model->group_model()->GetTabGroup(group_)->IsSaved();
save_group_toggle_->SetIsOn(is_saved);
}
auto* const new_tab_menu_item = AddChildView(CreateMenuItem(
TAB_GROUP_HEADER_CXMENU_NEW_TAB_IN_GROUP,
l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_NEW_TAB_IN_GROUP),
base::BindRepeating(&TabGroupEditorBubbleView::NewTabInGroupPressed,
base::Unretained(this)),
&kNewTabInGroupIcon));
AddChildView(CreateMenuItem(
TAB_GROUP_HEADER_CXMENU_UNGROUP,
l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_UNGROUP),
base::BindRepeating(&TabGroupEditorBubbleView::UngroupPressed,
base::Unretained(this), header_view),
&kUngroupIcon));
AddChildView(CreateMenuItem(
TAB_GROUP_HEADER_CXMENU_CLOSE_GROUP,
l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_CLOSE_GROUP),
base::BindRepeating(&TabGroupEditorBubbleView::CloseGroupPressed,
base::Unretained(this)),
&kCloseGroupIcon));
move_menu_item_ = AddChildView(
CreateMenuItem(TAB_GROUP_HEADER_CXMENU_MOVE_GROUP_TO_NEW_WINDOW,
l10n_util::GetStringUTF16(
IDS_TAB_GROUP_HEADER_CXMENU_MOVE_GROUP_TO_NEW_WINDOW),
base::BindRepeating(
&TabGroupEditorBubbleView::MoveGroupToNewWindowPressed,
base::Unretained(this)),
&kMoveGroupToNewWindowIcon));
move_menu_item_->SetEnabled(
tab_strip_model->count() !=
tab_strip_model->group_model()->GetTabGroup(group_)->tab_count());
// Setting up the layout.
const gfx::Insets control_insets = new_tab_menu_item->GetInsets();
const int vertical_spacing = control_insets.top();
const int horizontal_spacing = control_insets.left();
SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kVertical)
.SetInteriorMargin(gfx::Insets(vertical_spacing, 0));
title_field_->SetProperty(views::kMarginsKey,
gfx::Insets(vertical_spacing, horizontal_spacing));
color_selector_->SetProperty(views::kMarginsKey,
gfx::Insets(0, horizontal_spacing));
separator->SetProperty(views::kMarginsKey, gfx::Insets(vertical_spacing, 0));
// The save_group_line_container is only created if the
// feature::kTabGroupsSave is enabled.
if (save_group_line_container) {
gfx::Insets save_group_margins = control_insets;
const int label_height = new_tab_menu_item->GetPreferredSize().height();
const int control_height =
std::max(save_group_label->GetPreferredSize().height(),
save_group_toggle_->GetPreferredSize().height());
save_group_margins.set_top((label_height - control_height) / 2);
save_group_margins.set_bottom(save_group_margins.top());
save_group_icon->SetProperty(
views::kMarginsKey,
gfx::Insets(0, 0, 0, new_tab_menu_item->GetImageLabelSpacing()));
save_group_line_container
->SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kHorizontal)
.SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
.SetInteriorMargin(save_group_margins);
save_group_label->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
views::MaximumFlexSizeRule::kUnbounded));
}
}
TabGroupEditorBubbleView::~TabGroupEditorBubbleView() = default;
tab_groups::TabGroupColorId TabGroupEditorBubbleView::InitColorSet() {
const tab_groups::ColorLabelMap& color_map =
tab_groups::GetTabGroupColorLabelMap();
// TODO(tluk) remove the reliance on the ordering of the color pairs in the
// vector and use the ColorLabelMap structure instead.
std::copy(color_map.begin(), color_map.end(), std::back_inserter(colors_));
// Keep track of the current group's color, to be returned as the initial
// selected value.
auto* const group_model = browser_->tab_strip_model()->group_model();
return group_model->GetTabGroup(group_)->visual_data()->color();
}
void TabGroupEditorBubbleView::UpdateGroup() {
absl::optional<int> selected_element = color_selector_->GetSelectedElement();
TabGroup* tab_group =
browser_->tab_strip_model()->group_model()->GetTabGroup(group_);
const tab_groups::TabGroupVisualData* current_visual_data =
tab_group->visual_data();
const tab_groups::TabGroupColorId updated_color =
selected_element.has_value() ? colors_[selected_element.value()].first
: current_visual_data->color();
if (current_visual_data->color() != updated_color) {
base::RecordAction(
base::UserMetricsAction("TabGroups_TabGroupBubble_ColorChanged"));
}
tab_groups::TabGroupVisualData new_data(title_field_->GetText(),
updated_color,
current_visual_data->is_collapsed());
tab_group->SetVisualData(new_data, true);
}
void TabGroupEditorBubbleView::OnSaveTogglePressed() {
auto* group = browser_->tab_strip_model()->group_model()->GetTabGroup(group_);
if (save_group_toggle_->GetIsOn()) {
base::RecordAction(
base::UserMetricsAction("TabGroups_TabGroupBubble_GroupSaved"));
group->SaveGroup();
} else {
base::RecordAction(
base::UserMetricsAction("TabGroups_TabGroupBubble_GroupUnsaved"));
group->UnsaveGroup();
}
}
void TabGroupEditorBubbleView::NewTabInGroupPressed() {
base::RecordAction(
base::UserMetricsAction("TabGroups_TabGroupBubble_NewTabInGroup"));
TabStripModel* const model = browser_->tab_strip_model();
const auto tabs = model->group_model()->GetTabGroup(group_)->ListTabs();
model->delegate()->AddTabAt(GURL(), tabs.end(), true, group_);
// Close the widget to allow users to continue their work in their newly
// created tab.
GetWidget()->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
}
void TabGroupEditorBubbleView::UngroupPressed(TabGroupHeader* header_view) {
base::RecordAction(
base::UserMetricsAction("TabGroups_TabGroupBubble_Ungroup"));
if (header_view)
header_view->RemoveObserverFromWidget(GetWidget());
TabStripModel* const model = browser_->tab_strip_model();
const gfx::Range tab_range =
model->group_model()->GetTabGroup(group_)->ListTabs();
std::vector<int> tabs;
tabs.reserve(tab_range.length());
for (auto i = tab_range.start(); i < tab_range.end(); ++i)
tabs.push_back(i);
model->RemoveFromGroup(tabs);
// Close the widget because it is no longer applicable.
GetWidget()->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
}
void TabGroupEditorBubbleView::CloseGroupPressed() {
base::RecordAction(
base::UserMetricsAction("TabGroups_TabGroupBubble_CloseGroup"));
browser_->tab_strip_model()->CloseAllTabsInGroup(group_);
// Close the widget because it is no longer applicable.
GetWidget()->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
}
void TabGroupEditorBubbleView::MoveGroupToNewWindowPressed() {
browser_->tab_strip_model()->delegate()->MoveGroupToNewWindow(group_);
GetWidget()->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
}
void TabGroupEditorBubbleView::SendFeedbackPressed() {
base::RecordAction(
base::UserMetricsAction("TabGroups_TabGroupBubble_SendFeedback"));
chrome::ShowFeedbackPage(
browser_, chrome::FeedbackSource::kFeedbackSourceDesktopTabGroups,
/*description_template=*/std::string(),
/*description_placeholder_text=*/std::string(),
/*category_tag=*/std::string(),
/*extra_diagnostics=*/std::string());
GetWidget()->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
}
void TabGroupEditorBubbleView::OnBubbleClose() {
if (title_at_opening_ != title_field_->GetText()) {
base::RecordAction(
base::UserMetricsAction("TabGroups_TabGroupBubble_NameChanged"));
}
}
BEGIN_METADATA(TabGroupEditorBubbleView, views::BubbleDialogDelegateView)
END_METADATA
void TabGroupEditorBubbleView::TitleFieldController::ContentsChanged(
views::Textfield* sender,
const std::u16string& new_contents) {
DCHECK_EQ(sender, parent_->title_field_);
parent_->UpdateGroup();
}
bool TabGroupEditorBubbleView::TitleFieldController::HandleKeyEvent(
views::Textfield* sender,
const ui::KeyEvent& key_event) {
DCHECK_EQ(sender, parent_->title_field_);
// For special actions, only respond to key pressed events, to be consistent
// with other views like buttons and dialogs.
if (key_event.type() == ui::EventType::ET_KEY_PRESSED) {
const ui::KeyboardCode key_code = key_event.key_code();
if (key_code == ui::VKEY_ESCAPE) {
parent_->GetWidget()->CloseWithReason(
views::Widget::ClosedReason::kEscKeyPressed);
return true;
}
if (key_code == ui::VKEY_RETURN) {
parent_->GetWidget()->CloseWithReason(
views::Widget::ClosedReason::kUnspecified);
return true;
}
}
return false;
}
void TabGroupEditorBubbleView::TitleField::ShowContextMenu(
const gfx::Point& p,
ui::MenuSourceType source_type) {
// There is no easy way to stop the propagation of a ShowContextMenu event,
// which is sometimes used to open the bubble itself. So when the bubble is
// opened this way, we manually hide the textfield's context menu the first
// time. Otherwise, the textfield, which is automatically focused, would show
// an extra context menu when the bubble first opens.
if (stop_context_menu_propagation_) {
stop_context_menu_propagation_ = false;
return;
}
views::Textfield::ShowContextMenu(p, source_type);
}
BEGIN_METADATA(TabGroupEditorBubbleView, TitleField, views::Textfield)
END_METADATA
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(TabGroupEditorBubbleView,
kEditorBubbleIdentifier);