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