1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ash/system/tray/tray_detailed_view.h"
6
7 #include <utility>
8
9 #include "ash/public/cpp/ash_view_ids.h"
10 #include "ash/strings/grit/ash_strings.h"
11 #include "ash/style/ash_color_provider.h"
12 #include "ash/system/tray/detailed_view_delegate.h"
13 #include "ash/system/tray/hover_highlight_view.h"
14 #include "ash/system/tray/system_menu_button.h"
15 #include "ash/system/tray/tray_constants.h"
16 #include "ash/system/tray/tray_popup_utils.h"
17 #include "ash/system/tray/tri_view.h"
18 #include "base/containers/adapters.h"
19 #include "base/strings/string_number_conversions.h"
20 #include "third_party/skia/include/core/SkDrawLooper.h"
21 #include "ui/base/l10n/l10n_util.h"
22 #include "ui/base/resource/resource_bundle.h"
23 #include "ui/compositor/clip_recorder.h"
24 #include "ui/compositor/paint_context.h"
25 #include "ui/compositor/paint_recorder.h"
26 #include "ui/gfx/canvas.h"
27 #include "ui/gfx/geometry/insets.h"
28 #include "ui/gfx/image/image_skia.h"
29 #include "ui/gfx/paint_vector_icon.h"
30 #include "ui/gfx/skia_paint_util.h"
31 #include "ui/gfx/vector_icon_types.h"
32 #include "ui/views/accessibility/view_accessibility.h"
33 #include "ui/views/background.h"
34 #include "ui/views/border.h"
35 #include "ui/views/controls/image_view.h"
36 #include "ui/views/controls/label.h"
37 #include "ui/views/controls/progress_bar.h"
38 #include "ui/views/controls/scroll_view.h"
39 #include "ui/views/controls/separator.h"
40 #include "ui/views/layout/box_layout.h"
41 #include "ui/views/layout/fill_layout.h"
42 #include "ui/views/view_targeter.h"
43 #include "ui/views/view_targeter_delegate.h"
44 #include "ui/views/widget/widget.h"
45
46 namespace ash {
47 namespace {
48
49 // The index of the horizontal rule below the title row.
50 const int kTitleRowSeparatorIndex = 1;
51
52 // A view that is used as ScrollView contents. It supports designating some of
53 // the children as sticky header rows. The sticky header rows are not scrolled
54 // above the top of the visible viewport until the next one "pushes" it up and
55 // are painted above other children. To indicate that a child is a sticky header
56 // row use SetID(VIEW_ID_STICKY_HEADER).
57 class ScrollContentsView : public views::View {
58 public:
ScrollContentsView(DetailedViewDelegate * delegate)59 explicit ScrollContentsView(DetailedViewDelegate* delegate)
60 : delegate_(delegate) {
61 box_layout_ = SetLayoutManager(std::make_unique<views::BoxLayout>(
62 views::BoxLayout::Orientation::kVertical));
63 }
64 ~ScrollContentsView() override = default;
65
66 protected:
67 // views::View:
OnBoundsChanged(const gfx::Rect & previous_bounds)68 void OnBoundsChanged(const gfx::Rect& previous_bounds) override {
69 PositionHeaderRows();
70 }
71
PaintChildren(const views::PaintInfo & paint_info)72 void PaintChildren(const views::PaintInfo& paint_info) override {
73 int sticky_header_height = 0;
74 for (const auto& header : headers_) {
75 // Sticky header is at the top.
76 if (header.view->y() != header.natural_offset) {
77 sticky_header_height = header.view->bounds().height();
78 DCHECK_EQ(VIEW_ID_STICKY_HEADER, header.view->GetID());
79 break;
80 }
81 }
82 // Paint contents other than sticky headers. If sticky header is at the top,
83 // it clips the header's height so that nothing is shown behind the header.
84 {
85 ui::ClipRecorder clip_recorder(paint_info.context());
86 gfx::Rect clip_rect = gfx::Rect(paint_info.paint_recording_size()) -
87 paint_info.offset_from_parent();
88 gfx::Insets clip_insets(sticky_header_height, 0, 0, 0);
89 clip_rect.Inset(gfx::ScaleToFlooredInsets(
90 clip_insets, paint_info.paint_recording_scale_x(),
91 paint_info.paint_recording_scale_y()));
92 clip_recorder.ClipRect(clip_rect);
93 for (auto* child : children()) {
94 if (child->GetID() != VIEW_ID_STICKY_HEADER && !child->layer())
95 child->Paint(paint_info);
96 }
97 }
98 // Paint sticky headers.
99 for (auto* child : children()) {
100 if (child->GetID() == VIEW_ID_STICKY_HEADER && !child->layer())
101 child->Paint(paint_info);
102 }
103
104 bool did_draw_shadow = false;
105 // Paint header row separators.
106 for (auto& header : headers_)
107 did_draw_shadow =
108 PaintDelineation(header, paint_info.context()) || did_draw_shadow;
109
110 // Draw a shadow at the top of the viewport when scrolled, but only if a
111 // header didn't already draw one. Overlap the shadow with the separator
112 // that's below the header view so we don't get both a separator and a full
113 // shadow.
114 if (y() != 0 && !did_draw_shadow)
115 DrawShadow(paint_info.context(),
116 gfx::Rect(0, 0, width(), -y() - kTraySeparatorWidth));
117 }
118
Layout()119 void Layout() override {
120 views::View::Layout();
121 headers_.clear();
122 for (auto* child : children()) {
123 if (child->GetID() == VIEW_ID_STICKY_HEADER)
124 headers_.emplace_back(child);
125 }
126 PositionHeaderRows();
127 }
128
GetClassName() const129 const char* GetClassName() const override { return "ScrollContentsView"; }
130
GetChildrenInZOrder()131 View::Views GetChildrenInZOrder() override {
132 // Place sticky headers last in the child order so that they wind up on top
133 // in Z order.
134 View::Views children_in_z_order = children();
135 std::stable_partition(children_in_z_order.begin(),
136 children_in_z_order.end(), [](const View* child) {
137 return child->GetID() != VIEW_ID_STICKY_HEADER;
138 });
139 return children_in_z_order;
140 }
141
ViewHierarchyChanged(const views::ViewHierarchyChangedDetails & details)142 void ViewHierarchyChanged(
143 const views::ViewHierarchyChangedDetails& details) override {
144 if (!details.is_add && details.parent == this) {
145 headers_.erase(std::remove_if(headers_.begin(), headers_.end(),
146 [details](const Header& header) {
147 return header.view == details.child;
148 }),
149 headers_.end());
150 } else if (details.is_add && details.parent == this &&
151 details.child == children().front()) {
152 // We always want padding on the bottom of the scroll contents.
153 // We only want padding on the top of the scroll contents if the first
154 // child is not a header (in that case, the padding is built into the
155 // header).
156 DCHECK_EQ(box_layout_, GetLayoutManager());
157 box_layout_->set_inside_border_insets(
158 gfx::Insets(details.child->GetID() == VIEW_ID_STICKY_HEADER
159 ? 0
160 : kMenuSeparatorVerticalPadding,
161 0, kMenuSeparatorVerticalPadding, 0));
162 }
163 }
164
165 private:
166 const int kShadowOffsetY = 2;
167 const int kShadowBlur = 2;
168
169 // A structure that keeps the original offset of each header between the
170 // calls to Layout() to allow keeping track of which view should be sticky.
171 struct Header {
Headerash::__anoncdb2a8dc0111::ScrollContentsView::Header172 explicit Header(views::View* view)
173 : view(view), natural_offset(view->y()), draw_separator_below(false) {}
174
175 // A header View that can be decorated as sticky.
176 views::View* view;
177
178 // Offset from the top of ScrollContentsView to |view|'s original vertical
179 // position.
180 int natural_offset;
181
182 // True when a separator needs to be painted below the header when another
183 // header is pushing |this| header up.
184 bool draw_separator_below;
185 };
186
187 // Adjusts y-position of header rows allowing one or two rows to stick to the
188 // top of the visible viewport.
PositionHeaderRows()189 void PositionHeaderRows() {
190 const int scroll_offset = -y();
191 Header* previous_header = nullptr;
192 for (auto& header : base::Reversed(headers_)) {
193 views::View* header_view = header.view;
194 bool draw_separator_below = false;
195 if (header.natural_offset >= scroll_offset) {
196 previous_header = &header;
197 header_view->SetY(header.natural_offset);
198 } else {
199 if (previous_header && previous_header->view->y() <=
200 scroll_offset + header_view->height()) {
201 // Lower header displacing the header above.
202 draw_separator_below = true;
203 header_view->SetY(previous_header->view->y() - header_view->height());
204 } else {
205 // A header becomes sticky.
206 header_view->SetY(scroll_offset);
207 header_view->Layout();
208 header_view->SchedulePaint();
209 }
210 }
211 if (header.draw_separator_below != draw_separator_below) {
212 header.draw_separator_below = draw_separator_below;
213 delegate_->ShowStickyHeaderSeparator(header_view, draw_separator_below);
214 }
215 if (header.natural_offset < scroll_offset)
216 break;
217 }
218 }
219
220 // Paints a separator for a header view. The separator can be a horizontal
221 // rule or a horizontal shadow, depending on whether the header is sticking to
222 // the top of the scroll viewport. The return value indicates whether a shadow
223 // was drawn.
PaintDelineation(const Header & header,const ui::PaintContext & context)224 bool PaintDelineation(const Header& header, const ui::PaintContext& context) {
225 const View* view = header.view;
226
227 // If the header is where it normally belongs or If the header is pushed by
228 // a header directly below it, draw nothing.
229 if (view->y() == header.natural_offset || header.draw_separator_below)
230 return false;
231
232 // Otherwise, draw a shadow below.
233 DrawShadow(context,
234 gfx::Rect(0, 0, view->width(), view->bounds().bottom()));
235 return true;
236 }
237
238 // Draws a drop shadow below |shadowed_area|.
DrawShadow(const ui::PaintContext & context,const gfx::Rect & shadowed_area)239 void DrawShadow(const ui::PaintContext& context,
240 const gfx::Rect& shadowed_area) {
241 ui::PaintRecorder recorder(context, size());
242 gfx::Canvas* canvas = recorder.canvas();
243 cc::PaintFlags flags;
244 gfx::ShadowValues shadow;
245 shadow.emplace_back(
246 gfx::Vector2d(0, kShadowOffsetY), kShadowBlur,
247 AshColorProvider::Get()->GetContentLayerColor(
248 AshColorProvider::ContentLayerType::kSeparatorColor));
249 flags.setLooper(gfx::CreateShadowDrawLooper(shadow));
250 flags.setAntiAlias(true);
251 canvas->ClipRect(shadowed_area, SkClipOp::kDifference);
252 canvas->DrawRect(shadowed_area, flags);
253 }
254
255 DetailedViewDelegate* const delegate_;
256
257 views::BoxLayout* box_layout_ = nullptr;
258
259 // Header child views that stick to the top of visible viewport when scrolled.
260 std::vector<Header> headers_;
261
262 DISALLOW_COPY_AND_ASSIGN(ScrollContentsView);
263 };
264
265 } // namespace
266
267 ////////////////////////////////////////////////////////////////////////////////
268 // TrayDetailedView:
269
TrayDetailedView(DetailedViewDelegate * delegate)270 TrayDetailedView::TrayDetailedView(DetailedViewDelegate* delegate)
271 : delegate_(delegate) {
272 box_layout_ = SetLayoutManager(std::make_unique<views::BoxLayout>(
273 views::BoxLayout::Orientation::kVertical, kUnifiedDetailedViewPadding));
274 SetBackground(views::CreateSolidBackground(
275 delegate_->GetBackgroundColor().value_or(SK_ColorTRANSPARENT)));
276 }
277
278 TrayDetailedView::~TrayDetailedView() = default;
279
OnViewClicked(views::View * sender)280 void TrayDetailedView::OnViewClicked(views::View* sender) {
281 HandleViewClicked(sender);
282 }
283
CreateTitleRow(int string_id)284 void TrayDetailedView::CreateTitleRow(int string_id) {
285 DCHECK(!tri_view_);
286
287 tri_view_ = delegate_->CreateTitleRow(string_id);
288
289 back_button_ = delegate_->CreateBackButton(base::BindRepeating(
290 &TrayDetailedView::TransitionToMainView, base::Unretained(this)));
291 tri_view_->AddView(TriView::Container::START, back_button_);
292
293 AddChildViewAt(tri_view_, 0);
294 AddChildViewAt(delegate_->CreateTitleSeparator(), kTitleRowSeparatorIndex);
295
296 CreateExtraTitleRowButtons();
297 Layout();
298 }
299
CreateScrollableList()300 void TrayDetailedView::CreateScrollableList() {
301 DCHECK(!scroller_);
302 auto scroll_content = std::make_unique<ScrollContentsView>(delegate_);
303 scroller_ = AddChildView(std::make_unique<views::ScrollView>());
304 scroller_->SetDrawOverflowIndicator(delegate_->IsOverflowIndicatorEnabled());
305 scroll_content_ = scroller_->SetContents(std::move(scroll_content));
306 // TODO(varkha): Make the sticky rows work with EnableViewPortLayer().
307 scroller_->SetBackgroundColor(delegate_->GetBackgroundColor());
308
309 box_layout_->SetFlexForView(scroller_, 1);
310 }
311
AddScrollListChild(std::unique_ptr<views::View> child)312 void TrayDetailedView::AddScrollListChild(std::unique_ptr<views::View> child) {
313 scroll_content_->AddChildView(std::move(child));
314 }
315
AddScrollListItem(const gfx::VectorIcon & icon,const base::string16 & text)316 HoverHighlightView* TrayDetailedView::AddScrollListItem(
317 const gfx::VectorIcon& icon,
318 const base::string16& text) {
319 HoverHighlightView* item = delegate_->CreateScrollListItem(this, icon, text);
320 scroll_content_->AddChildView(item);
321 return item;
322 }
323
AddScrollListCheckableItem(const gfx::VectorIcon & icon,const base::string16 & text,bool checked,bool enterprise_managed)324 HoverHighlightView* TrayDetailedView::AddScrollListCheckableItem(
325 const gfx::VectorIcon& icon,
326 const base::string16& text,
327 bool checked,
328 bool enterprise_managed) {
329 HoverHighlightView* item = AddScrollListItem(icon, text);
330 if (enterprise_managed) {
331 item->SetAccessibleName(l10n_util::GetStringFUTF16(
332 IDS_ASH_ACCESSIBILITY_FEATURE_MANAGED, text));
333 }
334 TrayPopupUtils::InitializeAsCheckableRow(item, checked, enterprise_managed);
335 return item;
336 }
337
AddScrollListCheckableItem(const base::string16 & text,bool checked,bool enterprise_managed)338 HoverHighlightView* TrayDetailedView::AddScrollListCheckableItem(
339 const base::string16& text,
340 bool checked,
341 bool enterprise_managed) {
342 return AddScrollListCheckableItem(gfx::kNoneIcon, text, checked,
343 enterprise_managed);
344 }
345
SetupConnectedScrollListItem(HoverHighlightView * view)346 void TrayDetailedView::SetupConnectedScrollListItem(HoverHighlightView* view) {
347 SetupConnectedScrollListItem(view, base::nullopt /* battery_percentage */);
348 }
349
SetupConnectedScrollListItem(HoverHighlightView * view,base::Optional<uint8_t> battery_percentage)350 void TrayDetailedView::SetupConnectedScrollListItem(
351 HoverHighlightView* view,
352 base::Optional<uint8_t> battery_percentage) {
353 DCHECK(view->is_populated());
354
355 base::string16 status;
356
357 if (battery_percentage) {
358 view->SetSubText(l10n_util::GetStringFUTF16(
359 IDS_ASH_STATUS_TRAY_BLUETOOTH_DEVICE_CONNECTED_WITH_BATTERY_LABEL,
360 base::NumberToString16(battery_percentage.value())));
361 } else {
362 view->SetSubText(l10n_util::GetStringUTF16(
363 IDS_ASH_STATUS_TRAY_NETWORK_STATUS_CONNECTED));
364 }
365
366 view->sub_text_label()->SetAutoColorReadabilityEnabled(false);
367 view->sub_text_label()->SetEnabledColor(
368 AshColorProvider::Get()->GetContentLayerColor(
369 AshColorProvider::ContentLayerType::kTextColorPositive));
370 }
371
SetupConnectingScrollListItem(HoverHighlightView * view)372 void TrayDetailedView::SetupConnectingScrollListItem(HoverHighlightView* view) {
373 DCHECK(view->is_populated());
374
375 view->SetSubText(
376 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_NETWORK_STATUS_CONNECTING));
377 }
378
AddScrollListSubHeader(const gfx::VectorIcon & icon,int text_id)379 TriView* TrayDetailedView::AddScrollListSubHeader(const gfx::VectorIcon& icon,
380 int text_id) {
381 TriView* header = TrayPopupUtils::CreateSubHeaderRowView(true);
382 TrayPopupUtils::ConfigureAsStickyHeader(header);
383
384 auto* color_provider = AshColorProvider::Get();
385 views::Label* label = TrayPopupUtils::CreateDefaultLabel();
386 label->SetText(l10n_util::GetStringUTF16(text_id));
387 label->SetEnabledColor(color_provider->GetContentLayerColor(
388 AshColorProvider::ContentLayerType::kTextColorPrimary));
389 TrayPopupUtils::SetLabelFontList(label,
390 TrayPopupUtils::FontStyle::kSubHeader);
391 header->AddView(TriView::Container::CENTER, label);
392
393 views::ImageView* image_view = TrayPopupUtils::CreateMainImageView();
394 image_view->SetImage(gfx::CreateVectorIcon(
395 icon, color_provider->GetContentLayerColor(
396 AshColorProvider::ContentLayerType::kIconColorPrimary)));
397 header->AddView(TriView::Container::START, image_view);
398
399 scroll_content_->AddChildView(header);
400 return header;
401 }
402
AddScrollListSubHeader(int text_id)403 TriView* TrayDetailedView::AddScrollListSubHeader(int text_id) {
404 return AddScrollListSubHeader(gfx::kNoneIcon, text_id);
405 }
406
Reset()407 void TrayDetailedView::Reset() {
408 RemoveAllChildViews(true);
409 scroller_ = nullptr;
410 scroll_content_ = nullptr;
411 progress_bar_ = nullptr;
412 back_button_ = nullptr;
413 tri_view_ = nullptr;
414 }
415
ShowProgress(double value,bool visible)416 void TrayDetailedView::ShowProgress(double value, bool visible) {
417 DCHECK(tri_view_);
418 if (!progress_bar_) {
419 progress_bar_ = AddChildViewAt(
420 std::make_unique<views::ProgressBar>(kTitleRowProgressBarHeight),
421 kTitleRowSeparatorIndex + 1);
422 progress_bar_->GetViewAccessibility().OverrideName(
423 l10n_util::GetStringUTF16(
424 IDS_ASH_STATUS_TRAY_NETWORK_PROGRESS_ACCESSIBLE_NAME));
425 progress_bar_->SetVisible(false);
426 progress_bar_->SetForegroundColor(
427 AshColorProvider::Get()->GetContentLayerColor(
428 AshColorProvider::ContentLayerType::kIconColorProminent));
429 }
430
431 progress_bar_->SetValue(value);
432 progress_bar_->SetVisible(visible);
433 children()[size_t{kTitleRowSeparatorIndex}]->SetVisible(!visible);
434 }
435
CreateInfoButton(views::Button::PressedCallback callback,int info_accessible_name_id)436 views::Button* TrayDetailedView::CreateInfoButton(
437 views::Button::PressedCallback callback,
438 int info_accessible_name_id) {
439 return delegate_->CreateInfoButton(std::move(callback),
440 info_accessible_name_id);
441 }
442
CreateSettingsButton(views::Button::PressedCallback callback,int setting_accessible_name_id)443 views::Button* TrayDetailedView::CreateSettingsButton(
444 views::Button::PressedCallback callback,
445 int setting_accessible_name_id) {
446 return delegate_->CreateSettingsButton(std::move(callback),
447 setting_accessible_name_id);
448 }
449
CreateHelpButton(views::Button::PressedCallback callback)450 views::Button* TrayDetailedView::CreateHelpButton(
451 views::Button::PressedCallback callback) {
452 return delegate_->CreateHelpButton(std::move(callback));
453 }
454
CreateListSubHeaderSeparator()455 views::Separator* TrayDetailedView::CreateListSubHeaderSeparator() {
456 return delegate_->CreateListSubHeaderSeparator();
457 }
458
HandleViewClicked(views::View * view)459 void TrayDetailedView::HandleViewClicked(views::View* view) {
460 NOTREACHED();
461 }
462
CreateExtraTitleRowButtons()463 void TrayDetailedView::CreateExtraTitleRowButtons() {}
464
TransitionToMainView()465 void TrayDetailedView::TransitionToMainView() {
466 delegate_->TransitionToMainView(back_button_ && back_button_->HasFocus());
467 }
468
CloseBubble()469 void TrayDetailedView::CloseBubble() {
470 // widget may be null in tests, in this case we do not need to do anything.
471 views::Widget* widget = GetWidget();
472 if (!widget)
473 return;
474 // Don't close again if we're already closing.
475 if (widget->IsClosed())
476 return;
477 delegate_->CloseBubble();
478 }
479
Layout()480 void TrayDetailedView::Layout() {
481 views::View::Layout();
482 if (scroller_ && !scroller_->is_bounded())
483 scroller_->ClipHeightTo(0, scroller_->height());
484 }
485
GetHeightForWidth(int width) const486 int TrayDetailedView::GetHeightForWidth(int width) const {
487 if (bounds().IsEmpty())
488 return views::View::GetHeightForWidth(width);
489
490 // The height of the bubble that contains this detailed view is set to
491 // the preferred height of the default view, and that determines the
492 // initial height of |this|. Always request to stay the same height.
493 return height();
494 }
495
GetClassName() const496 const char* TrayDetailedView::GetClassName() const {
497 return "TrayDetailedView";
498 }
499
500 } // namespace ash
501