| // Copyright 2019 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/tabs/tab_group_header.h" |
| |
| #include <memory> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| |
| #include "base/feature_list.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "chrome/app/vector_icons/vector_icons.h" |
| #include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_element_identifiers.h" |
| #include "chrome/browser/ui/color/chrome_color_id.h" |
| #include "chrome/browser/ui/layout_constants.h" |
| #include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.h" |
| #include "chrome/browser/ui/tabs/tab_style.h" |
| #include "chrome/browser/ui/ui_features.h" |
| #include "chrome/browser/ui/views/tabs/tab_group_editor_bubble_view.h" |
| #include "chrome/browser/ui/views/tabs/tab_group_style.h" |
| #include "chrome/browser/ui/views/tabs/tab_group_underline.h" |
| #include "chrome/browser/ui/views/tabs/tab_slot_controller.h" |
| #include "chrome/browser/ui/views/tabs/tab_slot_view.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip_controller.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip_layout.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip_types.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/data_sharing/public/features.h" |
| #include "components/saved_tab_groups/public/features.h" |
| #include "components/saved_tab_groups/public/tab_group_sync_service.h" |
| #include "components/strings/grit/components_strings.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/skia/include/core/SkPath.h" |
| #include "third_party/skia/include/core/SkRRect.h" |
| #include "ui/accessibility/ax_node_data.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/mojom/menu_source_type.mojom-forward.h" |
| #include "ui/base/mojom/menu_source_type.mojom-shared.h" |
| #include "ui/color/color_id.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/geometry/skia_conversions.h" |
| #include "ui/gfx/vector_icon_types.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/background.h" |
| #include "ui/views/border.h" |
| #include "ui/views/controls/button/image_button_factory.h" |
| #include "ui/views/controls/focus_ring.h" |
| #include "ui/views/controls/highlight_path_generator.h" |
| #include "ui/views/controls/image_view.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/interaction/element_tracker_views.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/layout/flex_layout.h" |
| #include "ui/views/layout/flex_layout_types.h" |
| #include "ui/views/layout/layout_provider.h" |
| #include "ui/views/view.h" |
| #include "ui/views/view_class_properties.h" |
| |
| namespace { |
| |
| // The amount of padding between the label and the sync icon. |
| constexpr int kSyncIconPaddingFromLabel = 2; |
| |
| bool SupportsDataSharing() { |
| return data_sharing::features::IsDataSharingFunctionalityEnabled(); |
| } |
| |
| class TabGroupHighlightPathGenerator : public views::HighlightPathGenerator { |
| public: |
| TabGroupHighlightPathGenerator(const views::View* chip, |
| const views::View* title, |
| const TabGroupStyle& style) |
| : chip_(chip), title_(title), style_(style) {} |
| TabGroupHighlightPathGenerator(const TabGroupHighlightPathGenerator&) = |
| delete; |
| TabGroupHighlightPathGenerator& operator=( |
| const TabGroupHighlightPathGenerator&) = delete; |
| |
| // views::HighlightPathGenerator: |
| SkPath GetHighlightPath(const views::View* view) override { |
| return SkPath::RRect(SkRRect::MakeRectXY( |
| gfx::RectToSkRect(chip_->bounds()), |
| style_->GetHighlightPathGeneratorCornerRadius(title_), |
| style_->GetHighlightPathGeneratorCornerRadius(title_))); |
| } |
| |
| private: |
| const raw_ptr<const views::View, AcrossTasksDanglingUntriaged> chip_; |
| const raw_ptr<const views::View, AcrossTasksDanglingUntriaged> title_; |
| const raw_ref<const TabGroupStyle> style_; |
| }; |
| |
| } // namespace |
| |
| TabGroupHeader::TabGroupHeader(TabSlotController& tab_slot_controller, |
| const tab_groups::TabGroupId& group, |
| const TabGroupStyle& style) |
| : tab_slot_controller_(tab_slot_controller), |
| title_chip_(AddChildView(std::make_unique<views::View>())), |
| title_(title_chip_->AddChildView(std::make_unique<views::Label>())), |
| sync_icon_( |
| title_chip_->AddChildView(std::make_unique<views::ImageView>())), |
| attention_indicator_( |
| title_chip_->AddChildView(std::make_unique<views::ImageView>())), |
| group_style_(style), |
| tab_style_(TabStyle::Get()), |
| group_title_(u""), |
| color_(tab_slot_controller_->GetPaintedGroupColor( |
| tab_slot_controller_->GetGroupColorId(group))), |
| is_collapsed_(tab_slot_controller_->IsGroupCollapsed(group)), |
| editor_bubble_tracker_(tab_slot_controller) {} |
| |
| TabGroupHeader::~TabGroupHeader() = default; |
| |
| void TabGroupHeader::Init(const tab_groups::TabGroupId& group) { |
| SetGroup(group); |
| set_context_menu_controller(this); |
| |
| // Disable events processing (like tooltip handling) |
| // for children of TabGroupHeader. |
| title_chip_->SetCanProcessEventsWithinSubtree(false); |
| |
| title_->SetCollapseWhenHidden(true); |
| title_->SetAutoColorReadabilityEnabled(false); |
| title_->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| title_->SetElideBehavior(gfx::FADE_TAIL); |
| title_->SetLineHeight(20); |
| |
| // Enable keyboard focus. |
| SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY); |
| views::FocusRing::Install(this); |
| views::HighlightPathGenerator::Install( |
| this, std::make_unique<TabGroupHighlightPathGenerator>( |
| title_chip_, title_, *group_style_)); |
| // The tab group gets painted with a solid color that may not contrast well |
| // with the focus indicator, so draw an outline around the focus ring for it |
| // to contrast with the solid color. |
| SetProperty(views::kDrawFocusRingBackgroundOutline, true); |
| |
| SetProperty(views::kElementIdentifierKey, kTabGroupHeaderElementId); |
| attention_indicator_->SetProperty(views::kElementIdentifierKey, |
| kAttentionIndicatorViewElementId); |
| |
| SetEventTargeter(std::make_unique<views::ViewTargeter>(this)); |
| |
| SetCollapsedState(); |
| UpdateIsCollapsed(); |
| |
| GetViewAccessibility().SetRole(ax::mojom::Role::kTabList); |
| GetViewAccessibility().SetIsEditable(true); |
| |
| title_text_changed_subscription_ = |
| title_->AddTextChangedCallback(base::BindRepeating( |
| &TabGroupHeader::UpdateTooltipText, base::Unretained(this))); |
| |
| UpdateTooltipText(); |
| } |
| |
| bool TabGroupHeader::OnKeyPressed(const ui::KeyEvent& event) { |
| if ((event.key_code() == ui::VKEY_SPACE || |
| event.key_code() == ui::VKEY_RETURN) && |
| !editor_bubble_tracker_.is_open()) { |
| tab_slot_controller_->ToggleTabGroupCollapsedState( |
| group().value(), ToggleTabGroupCollapsedStateOrigin::kKeyboard); |
| NotifyAccessibilityEventDeprecated(ax::mojom::Event::kSelection, true); |
| return true; |
| } |
| |
| constexpr int kModifiedFlag = |
| #if BUILDFLAG(IS_MAC) |
| ui::EF_COMMAND_DOWN; |
| #else |
| ui::EF_CONTROL_DOWN; |
| #endif |
| |
| if (event.type() == ui::EventType::kKeyPressed && |
| (event.flags() & kModifiedFlag)) { |
| if (event.key_code() == ui::VKEY_RIGHT) { |
| tab_slot_controller_->ShiftGroupRight(group().value()); |
| return true; |
| } |
| if (event.key_code() == ui::VKEY_LEFT) { |
| tab_slot_controller_->ShiftGroupLeft(group().value()); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| bool TabGroupHeader::OnMousePressed(const ui::MouseEvent& event) { |
| // Ignore the click if the editor is already open. Do this so clicking |
| // on us again doesn't re-trigger the editor. |
| // |
| // Though the bubble is deactivated before we receive a mouse event, |
| // the actual widget destruction happens in a posted task. That task |
| // gets run after we receive the mouse event. If this sounds brittle, |
| // that's because it is! |
| if (editor_bubble_tracker_.is_open()) { |
| return false; |
| } |
| |
| // Allow a right click from touch to drag, which corresponds to a long click. |
| if (event.IsOnlyLeftMouseButton() || |
| (event.IsOnlyRightMouseButton() && event.flags() & ui::EF_FROM_TOUCH)) { |
| tab_slot_controller_->MaybeStartDrag( |
| this, event, tab_slot_controller_->GetSelectionModel()); |
| |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool TabGroupHeader::OnMouseDragged(const ui::MouseEvent& event) { |
| // TODO: ensure ignoring return value is ok. |
| std::ignore = tab_slot_controller_->ContinueDrag(this, event); |
| return true; |
| } |
| |
| void TabGroupHeader::OnMouseReleased(const ui::MouseEvent& event) { |
| if (!dragging()) { |
| bool open_editor_bubble = |
| base::FeatureList::IsEnabled(tab_groups::kLeftClickOpensTabGroupBubble) |
| ? (event.IsLeftMouseButton() && !editor_bubble_tracker_.is_open()) |
| : (event.IsRightMouseButton() && !editor_bubble_tracker_.is_open()); |
| bool toggle_collapse = |
| base::FeatureList::IsEnabled(tab_groups::kLeftClickOpensTabGroupBubble) |
| ? event.IsRightMouseButton() |
| : event.IsLeftMouseButton(); |
| |
| if (open_editor_bubble) { |
| editor_bubble_tracker_.Opened(TabGroupEditorBubbleView::Show( |
| tab_slot_controller_->GetBrowser(), group().value(), |
| /*anchor_view=*/this, /*anchor_rect=*/std::nullopt, |
| /*stop_context_menu_propagation=*/false)); |
| } else if (toggle_collapse) { |
| tab_slot_controller_->ToggleTabGroupCollapsedState( |
| group().value(), ToggleTabGroupCollapsedStateOrigin::kMouse); |
| } |
| } |
| |
| tab_slot_controller_->EndDrag(EndDragReason::kComplete); |
| } |
| |
| void TabGroupHeader::OnMouseEntered(const ui::MouseEvent& event) { |
| // Hide the hover card, since there currently isn't anything to display |
| // for a group. |
| tab_slot_controller_->UpdateHoverCard( |
| nullptr, TabSlotController::HoverCardUpdateType::kHover); |
| } |
| |
| void TabGroupHeader::OnThemeChanged() { |
| TabSlotView::OnThemeChanged(); |
| VisualsChanged(); |
| } |
| |
| void TabGroupHeader::OnGestureEvent(ui::GestureEvent* event) { |
| tab_slot_controller_->UpdateHoverCard( |
| nullptr, TabSlotController::HoverCardUpdateType::kEvent); |
| switch (event->type()) { |
| case ui::EventType::kGestureTap: |
| tab_slot_controller_->ToggleTabGroupCollapsedState( |
| group().value(), ToggleTabGroupCollapsedStateOrigin::kGesture); |
| break; |
| case ui::EventType::kGestureLongTap: { |
| editor_bubble_tracker_.Opened(TabGroupEditorBubbleView::Show( |
| tab_slot_controller_->GetBrowser(), group().value(), |
| /*anchor_view=*/this, /*anchor_rect=*/std::nullopt, |
| /*stop_context_menu_propagation=*/false)); |
| break; |
| } |
| case ui::EventType::kGestureScrollBegin: { |
| tab_slot_controller_->MaybeStartDrag( |
| this, *event, tab_slot_controller_->GetSelectionModel()); |
| break; |
| } |
| default: |
| break; |
| } |
| event->SetHandled(); |
| } |
| |
| void TabGroupHeader::OnFocus() { |
| View::OnFocus(); |
| tab_slot_controller_->UpdateHoverCard( |
| nullptr, TabSlotController::HoverCardUpdateType::kFocus); |
| } |
| |
| void TabGroupHeader::OnGroupContentsChanged() { |
| UpdateAccessibleName(); |
| UpdateTooltipText(); |
| } |
| |
| void TabGroupHeader::UpdateTooltipText() { |
| if (!group().has_value()) { |
| return; |
| } |
| |
| TabGroup* tab_group = tab_slot_controller_->GetTabGroup(group().value()); |
| if (!tab_group || tab_group->IsEmpty() || tab_group->ListTabs().is_empty()) { |
| return; |
| } |
| |
| if (!title_->GetText().empty()) { |
| SetTooltipText(l10n_util::GetStringFUTF16( |
| IDS_TAB_GROUPS_NAMED_GROUP_TOOLTIP, std::u16string(title_->GetText()), |
| tab_slot_controller_->GetGroupContentString(group().value()))); |
| } else { |
| SetTooltipText(l10n_util::GetStringFUTF16( |
| IDS_TAB_GROUPS_UNNAMED_GROUP_TOOLTIP, |
| tab_slot_controller_->GetGroupContentString(group().value()))); |
| } |
| } |
| |
| gfx::Rect TabGroupHeader::GetAnchorBoundsInScreen() const { |
| // Skip the insetting in TabSlotView::GetAnchorBoundsInScreen(). In this |
| // context insetting makes the anchored bubble partially cut into the tab |
| // outline. |
| // TODO(crbug.com/40803556): See if the layout of TabGroupHeader can be |
| // unified with tabs so that bounds do not need to be calculated differently |
| // between tabs and headers. As of writing this, hover cards to not cut into |
| // the tab outline but without this change TabGroupEditorBubbleView does. |
| return View::GetAnchorBoundsInScreen(); |
| } |
| |
| TabSlotView::ViewType TabGroupHeader::GetTabSlotViewType() const { |
| return TabSlotView::ViewType::kTabGroupHeader; |
| } |
| |
| TabSizeInfo TabGroupHeader::GetTabSizeInfo() const { |
| TabSizeInfo size_info; |
| // Group headers have a fixed width based on `title_`'s width. |
| const int width = GetDesiredWidth(); |
| size_info.pinned_tab_width = width; |
| size_info.min_active_width = width; |
| size_info.min_inactive_width = width; |
| size_info.standard_width = width; |
| return size_info; |
| } |
| |
| void TabGroupHeader::ShowContextMenuForViewImpl( |
| views::View* source, |
| const gfx::Point& point, |
| ui::mojom::MenuSourceType source_type) { |
| // Right click toggles ShowContextMenuForViewImpl, which we dont want to occur |
| // if the left click should toggle the context menu. |
| if ((source_type == ui::mojom::MenuSourceType::kMouse && |
| base::FeatureList::IsEnabled( |
| tab_groups::kLeftClickOpensTabGroupBubble)) || |
| editor_bubble_tracker_.is_open()) { |
| return; |
| } |
| |
| // When the context menu is triggered via keyboard, the keyboard event |
| // propagates to the textfield inside the Editor Bubble. In those cases, we |
| // want to tell the Editor Bubble to stop the event by setting |
| // stop_context_menu_propagation to true. |
| // |
| // However, when the context menu is triggered via mouse, the same event |
| // sequence doesn't happen. Stopping the context menu propagation in that case |
| // would artificially hide the textfield's context menu the first time the |
| // user tried to access it. So we don't want to stop the context menu |
| // propagation if this call is reached via mouse. |
| // |
| // Notably, event behavior with a mouse is inconsistent depending on |
| // OS. When not on Mac, the OnMouseReleased() event happens first and opens |
| // the Editor Bubble early, preempting the Show() call below. On Mac, the |
| // ShowContextMenu() event happens first and the Show() call is made here. |
| // |
| // So, because of the event order on non-Mac, and because there is no native |
| // way to open a context menu via keyboard on Mac, we assume that we've |
| // reached this function via mouse if and only if the current OS is Mac. |
| // Therefore, we don't stop the menu propagation in that case. |
| constexpr bool kStopContextMenuPropagation = |
| #if BUILDFLAG(IS_MAC) |
| false; |
| #else |
| true; |
| #endif |
| |
| editor_bubble_tracker_.Opened(TabGroupEditorBubbleView::Show( |
| tab_slot_controller_->GetBrowser(), group().value(), this, std::nullopt, |
| kStopContextMenuPropagation)); |
| } |
| |
| bool TabGroupHeader::DoesIntersectRect(const views::View* target, |
| const gfx::Rect& rect) const { |
| // Tab group headers are only highlighted with a tab shape while dragging, so |
| // visually the header is basically a rectangle between two tab separators. |
| // The distance from the endge of the view to the tab separator is half of the |
| // overlap distance. We should only accept events between the separators. |
| const views::Widget* widget = GetWidget(); |
| bool extend_hittest = widget->IsMaximized() || widget->IsFullscreen(); |
| |
| gfx::Rect contents_rect = GetLocalBounds(); |
| contents_rect.Inset(gfx::Insets::TLBR( |
| extend_hittest ? 0 : GetLayoutConstant(TAB_STRIP_PADDING), |
| tab_style_->GetTabOverlap() / 2, 0, tab_style_->GetTabOverlap() / 2)); |
| return contents_rect.Intersects(rect); |
| } |
| |
| int TabGroupHeader::GetDesiredWidth() const { |
| const int overlap_margin = group_style_->GetTabGroupViewOverlap() * 2; |
| return overlap_margin + title_chip_->width(); |
| } |
| |
| void TabGroupHeader::SetCollapsedState() { |
| const bool collapsed_state = |
| tab_slot_controller_->IsGroupCollapsed(group().value()); |
| if (is_collapsed_ != collapsed_state) { |
| is_collapsed_ = collapsed_state; |
| |
| const ui::ElementIdentifier element_id = |
| GetProperty(views::kElementIdentifierKey); |
| if (element_id) { |
| views::ElementTrackerViews::GetInstance()->NotifyViewActivated(element_id, |
| this); |
| } |
| } |
| } |
| |
| void TabGroupHeader::VisualsChanged() { |
| // TODO(crbug.com/372296676): Make TabGroupHeader observe the group for |
| // changes to cut down on the number of times we recalculate the view. |
| const tab_groups::TabGroupId tab_group_id = group().value(); |
| group_title_ = tab_slot_controller_->GetGroupTitle(tab_group_id); |
| color_ = tab_slot_controller_->GetPaintedGroupColor( |
| tab_slot_controller_->GetGroupColorId(tab_group_id)); |
| should_show_header_icon_ = ShouldShowHeaderIcon(); |
| |
| // Update collapsed state before changing any UI. |
| SetCollapsedState(); |
| |
| UpdateTitleView(); |
| UpdateSyncIconView(); |
| UpdateAttentionIndicatorView(); |
| if (group_title_.empty()) { |
| CreateHeaderWithoutTitle(); |
| } else { |
| CreateHeaderWithTitle(); |
| } |
| |
| if (views::FocusRing::Get(this)) { |
| views::FocusRing::Get(this)->DeprecatedLayoutImmediately(); |
| } |
| |
| UpdateIsCollapsed(); |
| UpdateAccessibleName(); |
| } |
| |
| void TabGroupHeader::UpdateAccessibleName() { |
| TabGroup* tab_group = tab_slot_controller_->GetTabGroup(group().value()); |
| if (tab_group && tab_group->ListTabs().length() == 0) { |
| return; |
| } |
| |
| std::u16string title(tab_slot_controller_->GetGroupTitle(group().value())); |
| std::u16string contents = |
| tab_slot_controller_->GetGroupContentString(group().value()); |
| std::u16string group_status = std::u16string(); |
| |
| // Windows screen reader properly announces the state set above in `node_data` |
| // and will read out the state change when the header's collapsed state is |
| // toggled. The state is added into the title for other platforms and the title |
| // will be reread with the updated state when the header's collapsed state is |
| // toggled. |
| #if !BUILDFLAG(IS_WIN) |
| bool is_collapsed = tab_slot_controller_->IsGroupCollapsed(group().value()); |
| group_status = is_collapsed |
| ? l10n_util::GetStringUTF16(IDS_GROUP_AX_LABEL_COLLAPSED) |
| : l10n_util::GetStringUTF16(IDS_GROUP_AX_LABEL_EXPANDED); |
| #endif |
| |
| std::u16string shared_state = u""; |
| |
| if (SupportsDataSharing() && should_show_header_icon_) { |
| shared_state = l10n_util::GetStringUTF16(IDS_SAVED_GROUP_AX_LABEL_SHARED); |
| |
| if (needs_attention_) { |
| group_status += u", " + l10n_util::GetStringUTF16( |
| DATA_SHARING_GROUP_LABEL_NEW_ACTIVITY); |
| } |
| } |
| |
| std::u16string final_name; |
| if (title.empty()) { |
| final_name = |
| l10n_util::GetStringFUTF16(IDS_GROUP_AX_LABEL_UNNAMED_GROUP_FORMAT, |
| shared_state, contents, group_status); |
| } else { |
| final_name = |
| l10n_util::GetStringFUTF16(IDS_GROUP_AX_LABEL_NAMED_GROUP_FORMAT, |
| shared_state, title, contents, group_status); |
| } |
| GetViewAccessibility().SetName(final_name); |
| } |
| |
| int TabGroupHeader::GetCollapsedHeaderWidth() const { |
| return GetTabSizeInfo().standard_width; |
| } |
| |
| bool TabGroupHeader::ShouldShowHeaderIcon() const { |
| const bool supports_shared_groups = SupportsDataSharing(); |
| if (!supports_shared_groups) { |
| return false; |
| } |
| |
| tab_groups::TabGroupSyncService* tab_group_service = |
| tab_slot_controller_->GetBrowser() |
| ? tab_groups::TabGroupSyncServiceFactory::GetForProfile( |
| tab_slot_controller_->GetBrowser()->profile()) |
| : nullptr; |
| if (!tab_group_service) { |
| return false; |
| } |
| |
| std::optional<tab_groups::SavedTabGroup> saved_group = |
| tab_group_service->GetGroup(group().value()); |
| if (!saved_group) { |
| return false; |
| } |
| |
| if (supports_shared_groups) { |
| // DataSharing shows a share icon if the group is shared. |
| return saved_group->is_shared_tab_group(); |
| } |
| |
| // Show the V1 sync icon. |
| return true; |
| } |
| |
| void TabGroupHeader::UpdateIsCollapsed() { |
| if (is_collapsed_) { |
| GetViewAccessibility().SetIsCollapsed(); |
| } else { |
| GetViewAccessibility().SetIsExpanded(); |
| } |
| } |
| |
| void TabGroupHeader::UpdateTitleView() { |
| title_->SetText(group_title_); |
| |
| if (!group_title_.empty()) { |
| title_->SetEnabledColor(color_utils::GetColorWithMaxContrast(color_)); |
| } |
| } |
| |
| void TabGroupHeader::UpdateSyncIconView() { |
| sync_icon_->SetVisible(should_show_header_icon_); |
| if (should_show_header_icon_) { |
| bool use_share_icon = SupportsDataSharing(); |
| sync_icon_->SetImage(ui::ImageModel::FromVectorIcon( |
| use_share_icon ? kPeopleGroupIcon : kTabGroupsSyncIcon, |
| color_utils::GetColorWithMaxContrast(color_), |
| group_style_->GetSyncIconWidth())); |
| } |
| } |
| |
| void TabGroupHeader::UpdateAttentionIndicatorView() { |
| const bool supports_attention_indicator = SupportsDataSharing(); |
| if (!supports_attention_indicator) { |
| attention_indicator_->SetVisible(false); |
| return; |
| } |
| |
| const bool should_show_attention_indicator = GetShowingAttentionIndicator(); |
| attention_indicator_->SetVisible(should_show_attention_indicator); |
| if (should_show_attention_indicator) { |
| attention_indicator_->SetImage(ui::ImageModel::FromVectorIcon( |
| kDefaultTouchFaviconMaskIcon, |
| color_utils::GetColorWithMaxContrast(color_), |
| group_style_->GetAttentionIndicatorWidth())); |
| } |
| } |
| |
| std::u16string_view TabGroupHeader::GetTitleTextForTesting() const { |
| CHECK(title_); |
| return title_->GetText(); |
| } |
| |
| void TabGroupHeader::CreateHeaderWithoutTitle() { |
| title_chip_->SetBoundsRect(group_style_->GetEmptyTitleChipBounds(this)); |
| title_chip_->SetBackground(group_style_->GetEmptyTitleChipBackground(color_)); |
| |
| const int sync_icon_width = group_style_->GetSyncIconWidth(); |
| |
| if (should_show_header_icon_) { |
| const bool should_show_attention_indicator = GetShowingAttentionIndicator(); |
| if (should_show_attention_indicator) { |
| const gfx::Insets title_chip_insets = |
| group_style_->GetInsetsForHeaderChip(); |
| const int title_chip_vertical_inset = 0; |
| gfx::Rect title_chip_bounds = group_style_->GetEmptyTitleChipBounds(this); |
| const int attention_indicator_width = |
| group_style_->GetAttentionIndicatorWidth(); |
| |
| // The total width of the title chip includes the horizontal |
| // insets, the sync icon, and the attention indicator + its padding. |
| title_chip_bounds.set_width(sync_icon_width + attention_indicator_width + |
| kSyncIconPaddingFromLabel + |
| title_chip_insets.width()); |
| title_chip_->SetBoundsRect(title_chip_bounds); |
| |
| sync_icon_->SetBounds(title_chip_insets.left(), title_chip_vertical_inset, |
| sync_icon_width, title_chip_bounds.height()); |
| |
| attention_indicator_->SetBounds( |
| sync_icon_->bounds().right() + kSyncIconPaddingFromLabel, |
| title_chip_vertical_inset, attention_indicator_width, |
| title_chip_bounds.height()); |
| } else { |
| // The `sync_icon` by itself should be centered in the title chip. |
| gfx::Rect sync_icon_bounds = title_chip_->GetLocalBounds(); |
| sync_icon_bounds.ClampToCenteredSize( |
| gfx::Size(sync_icon_width, sync_icon_width)); |
| sync_icon_->SetBoundsRect(sync_icon_bounds); |
| } |
| } else { |
| sync_icon_->SetBounds(0, 0, 0, 0); |
| attention_indicator_->SetBounds(0, 0, 0, 0); |
| } |
| } |
| |
| void TabGroupHeader::CreateHeaderWithTitle() { |
| // TODO(crbug.com/40893761): The math of the layout in this function is done |
| // arithmetically and can be hard to understand. This should instead be done |
| // by a layout manager. |
| // Visual representation of tab group header: |
| // [ Total Content Width ] |
| // [Left Inset][Sync Icon][Padding][Group Title][Attention][Right Inset] |
| const int sync_icon_width = |
| should_show_header_icon_ ? group_style_->GetSyncIconWidth() : 0; |
| const int padding_between_label_sync_icon = |
| should_show_header_icon_ ? kSyncIconPaddingFromLabel : 0; |
| // Only show attention indicator if header icon will show and |
| // attention indicator is enabled. |
| const bool should_show_attention_indicator = |
| should_show_header_icon_ && GetShowingAttentionIndicator(); |
| const int attention_indicator_width = |
| should_show_attention_indicator |
| ? group_style_->GetAttentionIndicatorWidth() + |
| kSyncIconPaddingFromLabel |
| : 0; |
| const int attention_indicator_padding = |
| should_show_attention_indicator ? kSyncIconPaddingFromLabel : 0; |
| |
| // The max width of the content should be half the standard tab width (not |
| // counting overlap). |
| const int text_max_width = (tab_style_->GetStandardWidth(/*is_split*/ false) - |
| tab_style_->GetTabOverlap()) / |
| 2 - |
| sync_icon_width - padding_between_label_sync_icon; |
| const int text_width = std::min( |
| title_->GetPreferredSize(views::SizeBounds(title_->width(), {})).width(), |
| text_max_width); |
| const int text_height = |
| title_->GetPreferredSize(views::SizeBounds(title_->width(), {})).height(); |
| |
| // Width of title chip should at least be the width of an empty title chip. |
| const int total_content_width = |
| sync_icon_width + padding_between_label_sync_icon + text_width + |
| attention_indicator_width + attention_indicator_padding; |
| const gfx::Insets title_chip_insets = group_style_->GetInsetsForHeaderChip(); |
| const int title_chip_width = |
| std::max(group_style_->GetEmptyTitleChipBounds(this).width(), |
| total_content_width + title_chip_insets.width()); |
| |
| // The title chip's radius should nestle snuggly against the tab corner |
| // radius, taking into account the group underline stroke. |
| const gfx::Point title_chip_origin = |
| group_style_->GetTitleChipOffset(text_height); |
| const int corner_radius = group_style_->GetChipCornerRadius(); |
| title_chip_->SetBounds(title_chip_origin.x(), title_chip_origin.y(), |
| title_chip_width, text_height); |
| title_chip_->SetBackground( |
| views::CreateRoundedRectBackground(color_, corner_radius)); |
| |
| // Set the bounds of the sync icon first, followed by the title. |
| const int start_of_sync_icon = title_chip_insets.left(); |
| const int title_chip_vertical_inset = 0; |
| if (!should_show_header_icon_) { |
| sync_icon_->SetBounds(0, 0, 0, 0); |
| title_->SetBounds(title_chip_insets.left(), title_chip_vertical_inset, |
| text_width, text_height); |
| attention_indicator_->SetBounds(0, 0, 0, 0); |
| } else { |
| sync_icon_->SetBounds(start_of_sync_icon, title_chip_vertical_inset, |
| sync_icon_width, text_height); |
| title_->SetBounds( |
| sync_icon_->bounds().right() + padding_between_label_sync_icon, |
| title_chip_vertical_inset, text_width, text_height); |
| if (should_show_attention_indicator) { |
| attention_indicator_->SetBounds( |
| title_->bounds().right() + kSyncIconPaddingFromLabel, |
| title_chip_vertical_inset, attention_indicator_width, text_height); |
| } else { |
| attention_indicator_->SetBounds(0, 0, 0, 0); |
| } |
| } |
| } |
| |
| void TabGroupHeader::RemoveObserverFromWidget(views::Widget* widget) { |
| widget->RemoveObserver(&editor_bubble_tracker_); |
| } |
| |
| bool TabGroupHeader::GetShowingAttentionIndicator() { |
| // Attention should only be shown if the group is collapsed. |
| return is_collapsed_ && needs_attention_; |
| } |
| |
| void TabGroupHeader::SetTabGroupNeedsAttention(bool needs_attention) { |
| const bool supports_attention_indicator = SupportsDataSharing(); |
| if (!supports_attention_indicator) { |
| return; |
| } |
| |
| if (needs_attention_ != needs_attention) { |
| needs_attention_ = needs_attention; |
| VisualsChanged(); |
| } |
| } |
| |
| BEGIN_METADATA(TabGroupHeader) |
| ADD_READONLY_PROPERTY_METADATA(int, DesiredWidth) |
| END_METADATA |
| |
| TabGroupHeader::EditorBubbleTracker::EditorBubbleTracker( |
| TabSlotController& tab_slot_controller) |
| : tab_slot_controller_(tab_slot_controller) {} |
| |
| TabGroupHeader::EditorBubbleTracker::~EditorBubbleTracker() { |
| if (is_open_ && widget_) { |
| widget_->RemoveObserver(this); |
| widget_->Close(); |
| tab_slot_controller_->NotifyTabstripBubbleClosed(); |
| } |
| CHECK(!IsInObserverList()); |
| } |
| |
| void TabGroupHeader::EditorBubbleTracker::Opened(views::Widget* bubble_widget) { |
| DCHECK(bubble_widget); |
| DCHECK(!is_open_); |
| widget_ = bubble_widget; |
| is_open_ = true; |
| bubble_widget->AddObserver(this); |
| tab_slot_controller_->NotifyTabstripBubbleOpened(); |
| } |
| |
| void TabGroupHeader::EditorBubbleTracker::OnWidgetDestroying( |
| views::Widget* bubble_widget) { |
| CHECK(widget_ == bubble_widget); |
| is_open_ = false; |
| widget_->RemoveObserver(this); |
| widget_ = nullptr; |
| tab_slot_controller_->NotifyTabstripBubbleClosed(); |
| } |
| |
| DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(TabGroupHeader, |
| kAttentionIndicatorViewElementId); |