-
Notifications
You must be signed in to change notification settings - Fork 6.6k
/
tab_group_header.cc
462 lines (399 loc) · 16.7 KB
/
tab_group_header.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
// 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 "chrome/browser/ui/views/tabs/tab_group_header.h"
#include <memory>
#include <utility>
#include "base/feature_list.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/tabs/tab_controller.h"
#include "chrome/browser/ui/views/tabs/tab_group_editor_bubble_view.h"
#include "chrome/browser/ui/views/tabs/tab_group_underline.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/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/SkColor.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/label.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/metadata/metadata_impl_macros.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
namespace {
constexpr int kEmptyChipSize = 14;
int GetChipCornerRadius() {
return TabStyle::GetCornerRadius() - TabGroupUnderline::kStrokeThickness;
}
class TabGroupHighlightPathGenerator : public views::HighlightPathGenerator {
public:
TabGroupHighlightPathGenerator(const views::View* chip,
const views::View* title)
: chip_(chip), title_(title) {}
TabGroupHighlightPathGenerator(const TabGroupHighlightPathGenerator&) =
delete;
TabGroupHighlightPathGenerator& operator=(
const TabGroupHighlightPathGenerator&) = delete;
// views::HighlightPathGenerator:
SkPath GetHighlightPath(const views::View* view) override {
SkScalar corner_radius =
title_->GetVisible() ? GetChipCornerRadius() : kEmptyChipSize / 2;
return SkPath().addRoundRect(gfx::RectToSkRect(chip_->bounds()),
corner_radius, corner_radius);
}
private:
const views::View* const chip_;
const views::View* const title_;
};
} // namespace
TabGroupHeader::TabGroupHeader(TabStrip* tab_strip,
const tab_groups::TabGroupId& group)
: tab_strip_(tab_strip) {
DCHECK(tab_strip);
set_group(group);
set_context_menu_controller(this);
// The size and color of the chip are set in VisualsChanged().
title_chip_ = AddChildView(std::make_unique<views::View>());
// The text and color of the title are set in VisualsChanged().
title_ = title_chip_->AddChildView(std::make_unique<views::Label>());
title_->SetCollapseWhenHidden(true);
title_->SetAutoColorReadabilityEnabled(false);
title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
title_->SetElideBehavior(gfx::FADE_TAIL);
// Enable keyboard focus.
SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
focus_ring_ = views::FocusRing::Install(this);
views::HighlightPathGenerator::Install(
this,
std::make_unique<TabGroupHighlightPathGenerator>(title_chip_, title_));
SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
last_modified_expansion_ = base::TimeTicks::Now();
}
TabGroupHeader::~TabGroupHeader() {
LogCollapseTime();
}
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()) {
bool successful_toggle =
tab_strip_->controller()->ToggleTabGroupCollapsedState(
group().value(), ToggleTabGroupCollapsedStateOrigin::kKeyboard);
if (successful_toggle) {
#if defined(OS_WIN)
NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true);
#else
NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
#endif
LogCollapseTime();
}
return true;
}
constexpr int kModifiedFlag =
#if defined(OS_MAC)
ui::EF_COMMAND_DOWN;
#else
ui::EF_CONTROL_DOWN;
#endif
if (event.type() == ui::ET_KEY_PRESSED && (event.flags() & kModifiedFlag)) {
if (event.key_code() == ui::VKEY_RIGHT) {
tab_strip_->ShiftGroupRight(group().value());
return true;
}
if (event.key_code() == ui::VKEY_LEFT) {
tab_strip_->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;
tab_strip_->MaybeStartDrag(this, event, tab_strip_->GetSelectionModel());
return true;
}
bool TabGroupHeader::OnMouseDragged(const ui::MouseEvent& event) {
tab_strip_->ContinueDrag(this, event);
return true;
}
void TabGroupHeader::OnMouseReleased(const ui::MouseEvent& event) {
if (dragging()) {
tab_strip_->EndDrag(END_DRAG_COMPLETE);
return;
}
if (event.IsLeftMouseButton()) {
bool successful_toggle =
tab_strip_->controller()->ToggleTabGroupCollapsedState(
group().value(), ToggleTabGroupCollapsedStateOrigin::kMouse);
if (successful_toggle)
LogCollapseTime();
} else if (event.IsRightMouseButton() && !editor_bubble_tracker_.is_open()) {
editor_bubble_tracker_.Opened(TabGroupEditorBubbleView::Show(
tab_strip_->controller()->GetBrowser(), group().value(), this));
}
}
void TabGroupHeader::OnMouseEntered(const ui::MouseEvent& event) {
// Hide the hover card, since there currently isn't anything to display
// for a group.
tab_strip_->UpdateHoverCard(nullptr);
}
void TabGroupHeader::OnThemeChanged() {
TabSlotView::OnThemeChanged();
VisualsChanged();
}
void TabGroupHeader::OnGestureEvent(ui::GestureEvent* event) {
tab_strip_->UpdateHoverCard(nullptr);
switch (event->type()) {
case ui::ET_GESTURE_TAP: {
bool successful_toggle =
tab_strip_->controller()->ToggleTabGroupCollapsedState(
group().value(), ToggleTabGroupCollapsedStateOrigin::kGesture);
if (successful_toggle)
LogCollapseTime();
break;
}
case ui::ET_GESTURE_LONG_TAP: {
editor_bubble_tracker_.Opened(TabGroupEditorBubbleView::Show(
tab_strip_->controller()->GetBrowser(), group().value(), this));
break;
}
case ui::ET_GESTURE_SCROLL_BEGIN: {
tab_strip_->MaybeStartDrag(this, *event, tab_strip_->GetSelectionModel());
break;
}
default:
break;
}
event->SetHandled();
}
void TabGroupHeader::OnFocus() {
View::OnFocus();
tab_strip_->UpdateHoverCard(nullptr);
}
void TabGroupHeader::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->role = ax::mojom::Role::kTabList;
node_data->AddState(ax::mojom::State::kEditable);
bool is_collapsed =
tab_strip_->controller()->IsGroupCollapsed(group().value());
if (is_collapsed) {
node_data->AddState(ax::mojom::State::kCollapsed);
node_data->RemoveState(ax::mojom::State::kExpanded);
} else {
node_data->AddState(ax::mojom::State::kExpanded);
node_data->RemoveState(ax::mojom::State::kCollapsed);
}
base::string16 title =
tab_strip_->controller()->GetGroupTitle(group().value());
base::string16 contents =
tab_strip_->controller()->GetGroupContentString(group().value());
base::string16 collapsed_state = base::string16();
// 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 !defined(OS_WIN)
collapsed_state =
is_collapsed ? l10n_util::GetStringUTF16(IDS_GROUP_AX_LABEL_COLLAPSED)
: l10n_util::GetStringUTF16(IDS_GROUP_AX_LABEL_EXPANDED);
#endif
if (title.empty()) {
node_data->SetName(l10n_util::GetStringFUTF16(
IDS_GROUP_AX_LABEL_UNNAMED_GROUP_FORMAT, contents, collapsed_state));
} else {
node_data->SetName(
l10n_util::GetStringFUTF16(IDS_GROUP_AX_LABEL_NAMED_GROUP_FORMAT, title,
contents, collapsed_state));
}
}
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::MenuSourceType source_type) {
if (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 defined(OS_MAC)
false;
#else
true;
#endif
editor_bubble_tracker_.Opened(TabGroupEditorBubbleView::Show(
tab_strip_->controller()->GetBrowser(), group().value(), this,
base::nullopt, nullptr, 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.
gfx::Rect contents_rect = GetLocalBounds();
contents_rect.Inset(TabStyle::GetTabOverlap() / 2, 0);
return contents_rect.Intersects(rect);
}
int TabGroupHeader::GetDesiredWidth() const {
// If the tab group is collapsed, we want the right margin of the title to
// match the left margin. The left margin is always the group stroke inset.
// Using these values also guarantees the chip aligns with the collapsed
// stroke.
if (tab_strip_->controller()->IsGroupCollapsed(group().value()))
return title_chip_->width() + 2 * TabGroupUnderline::GetStrokeInset();
// We don't want tabs to visually overlap group headers, so we add that space
// to the width to compensate. We don't want to actually remove the overlap
// during layout however; that would cause an the margin to be visually uneven
// when the header is in the first slot and thus wouldn't overlap anything to
// the left.
const int overlap_margin = TabStyle::GetTabOverlap() * 2;
// The empty and non-empty chips have different sizes and corner radii, but
// both should look nestled against the group stroke of the tab to the right.
// This requires a +/- 2px adjustment to the width, which causes the tab to
// the right to be positioned in the right spot.
const base::string16 title =
tab_strip_->controller()->GetGroupTitle(group().value());
const int right_adjust = title.empty() ? 2 : -2;
return overlap_margin + title_chip_->width() + right_adjust;
}
void TabGroupHeader::LogCollapseTime() {
base::TimeTicks current_time = base::TimeTicks::Now();
if (tab_strip_->controller()->IsGroupCollapsed(group().value())) {
UMA_HISTOGRAM_LONG_TIMES_100("TabGroups.TimeSpentExpanded",
current_time - last_modified_expansion_);
} else {
UMA_HISTOGRAM_LONG_TIMES_100("TabGroups.TimeSpentCollapsed",
current_time - last_modified_expansion_);
}
last_modified_expansion_ = current_time;
}
void TabGroupHeader::VisualsChanged() {
const base::string16 title =
tab_strip_->controller()->GetGroupTitle(group().value());
const tab_groups::TabGroupColorId color_id =
tab_strip_->controller()->GetGroupColorId(group().value());
const SkColor color = tab_strip_->GetPaintedGroupColor(color_id);
if (title.empty()) {
// If the title is empty, the chip is just a circle.
title_->SetVisible(false);
const int y = (GetLayoutConstant(TAB_HEIGHT) - kEmptyChipSize) / 2;
title_chip_->SetBounds(TabGroupUnderline::GetStrokeInset(), y,
kEmptyChipSize, kEmptyChipSize);
title_chip_->SetBackground(
views::CreateRoundedRectBackground(color, kEmptyChipSize / 2));
} else {
// If the title is set, the chip is a rounded rect that matches the active
// tab shape, particularly the tab's corner radius.
title_->SetVisible(true);
title_->SetEnabledColor(color_utils::GetColorWithMaxContrast(color));
title_->SetText(title);
// Set the radius such that the chip nestles snugly against the tab corner
// radius, taking into account the group underline stroke.
const int corner_radius = GetChipCornerRadius();
// Clamp the width to a maximum of half the standard tab width (not counting
// overlap).
const int max_width =
(TabStyle::GetStandardWidth() - TabStyle::GetTabOverlap()) / 2;
const int text_width =
std::min(title_->GetPreferredSize().width(), max_width);
const int text_height = title_->GetPreferredSize().height();
const int text_vertical_inset = 1;
const int text_horizontal_inset = corner_radius + text_vertical_inset;
const int y =
(GetLayoutConstant(TAB_HEIGHT) - text_height) / 2 - text_vertical_inset;
title_chip_->SetBounds(TabGroupUnderline::GetStrokeInset(), y,
text_width + 2 * text_horizontal_inset,
text_height + 2 * text_vertical_inset);
title_chip_->SetBackground(
views::CreateRoundedRectBackground(color, corner_radius));
title_->SetBounds(text_horizontal_inset, text_vertical_inset, text_width,
text_height);
}
if (focus_ring_)
focus_ring_->Layout();
}
void TabGroupHeader::RemoveObserverFromWidget(views::Widget* widget) {
widget->RemoveObserver(&editor_bubble_tracker_);
}
BEGIN_METADATA(TabGroupHeader, views::View)
ADD_READONLY_PROPERTY_METADATA(int, DesiredWidth)
END_METADATA
TabGroupHeader::EditorBubbleTracker::~EditorBubbleTracker() {
if (is_open_) {
widget_->RemoveObserver(this);
widget_->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
}
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);
}
void TabGroupHeader::EditorBubbleTracker::OnWidgetDestroyed(
views::Widget* widget) {
is_open_ = false;
}