| // Copyright (c) 2012 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 "ash/system/tray/tray_detailed_view.h" |
| |
| #include <utility> |
| |
| #include "ash/public/cpp/ash_view_ids.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/style/ash_color_provider.h" |
| #include "ash/system/tray/detailed_view_delegate.h" |
| #include "ash/system/tray/hover_highlight_view.h" |
| #include "ash/system/tray/system_menu_button.h" |
| #include "ash/system/tray/tray_constants.h" |
| #include "ash/system/tray/tray_popup_utils.h" |
| #include "ash/system/tray/tri_view.h" |
| #include "base/bind.h" |
| #include "base/containers/adapters.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "third_party/skia/include/core/SkDrawLooper.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/compositor/clip_recorder.h" |
| #include "ui/compositor/paint_context.h" |
| #include "ui/compositor/paint_recorder.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/gfx/skia_paint_util.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/image_view.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/controls/progress_bar.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/controls/separator.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/view_targeter.h" |
| #include "ui/views/view_targeter_delegate.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace ash { |
| namespace { |
| |
| // The index of the horizontal rule below the title row. |
| const int kTitleRowSeparatorIndex = 1; |
| |
| // A view that is used as ScrollView contents. It supports designating some of |
| // the children as sticky header rows. The sticky header rows are not scrolled |
| // above the top of the visible viewport until the next one "pushes" it up and |
| // are painted above other children. To indicate that a child is a sticky header |
| // row use SetID(VIEW_ID_STICKY_HEADER). |
| class ScrollContentsView : public views::View { |
| public: |
| explicit ScrollContentsView(DetailedViewDelegate* delegate) |
| : delegate_(delegate) { |
| box_layout_ = SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kVertical)); |
| } |
| ~ScrollContentsView() override = default; |
| |
| protected: |
| // views::View: |
| void OnBoundsChanged(const gfx::Rect& previous_bounds) override { |
| PositionHeaderRows(); |
| } |
| |
| void PaintChildren(const views::PaintInfo& paint_info) override { |
| int sticky_header_height = 0; |
| for (const auto& header : headers_) { |
| // Sticky header is at the top. |
| if (header.view->y() != header.natural_offset) { |
| sticky_header_height = header.view->bounds().height(); |
| DCHECK_EQ(VIEW_ID_STICKY_HEADER, header.view->GetID()); |
| break; |
| } |
| } |
| // Paint contents other than sticky headers. If sticky header is at the top, |
| // it clips the header's height so that nothing is shown behind the header. |
| { |
| ui::ClipRecorder clip_recorder(paint_info.context()); |
| gfx::Rect clip_rect = gfx::Rect(paint_info.paint_recording_size()) - |
| paint_info.offset_from_parent(); |
| gfx::Insets clip_insets(sticky_header_height, 0, 0, 0); |
| clip_rect.Inset(gfx::ScaleToFlooredInsets( |
| clip_insets, paint_info.paint_recording_scale_x(), |
| paint_info.paint_recording_scale_y())); |
| clip_recorder.ClipRect(clip_rect); |
| for (auto* child : children()) { |
| if (child->GetID() != VIEW_ID_STICKY_HEADER && !child->layer()) |
| child->Paint(paint_info); |
| } |
| } |
| // Paint sticky headers. |
| for (auto* child : children()) { |
| if (child->GetID() == VIEW_ID_STICKY_HEADER && !child->layer()) |
| child->Paint(paint_info); |
| } |
| |
| bool did_draw_shadow = false; |
| // Paint header row separators. |
| for (auto& header : headers_) |
| did_draw_shadow = |
| PaintDelineation(header, paint_info.context()) || did_draw_shadow; |
| |
| // Draw a shadow at the top of the viewport when scrolled, but only if a |
| // header didn't already draw one. Overlap the shadow with the separator |
| // that's below the header view so we don't get both a separator and a full |
| // shadow. |
| if (y() != 0 && !did_draw_shadow) |
| DrawShadow(paint_info.context(), |
| gfx::Rect(0, 0, width(), -y() - kTraySeparatorWidth)); |
| } |
| |
| void Layout() override { |
| views::View::Layout(); |
| headers_.clear(); |
| for (auto* child : children()) { |
| if (child->GetID() == VIEW_ID_STICKY_HEADER) |
| headers_.emplace_back(child); |
| } |
| PositionHeaderRows(); |
| } |
| |
| const char* GetClassName() const override { return "ScrollContentsView"; } |
| |
| View::Views GetChildrenInZOrder() override { |
| // Place sticky headers last in the child order so that they wind up on top |
| // in Z order. |
| View::Views children_in_z_order = children(); |
| std::stable_partition(children_in_z_order.begin(), |
| children_in_z_order.end(), [](const View* child) { |
| return child->GetID() != VIEW_ID_STICKY_HEADER; |
| }); |
| return children_in_z_order; |
| } |
| |
| void ViewHierarchyChanged( |
| const views::ViewHierarchyChangedDetails& details) override { |
| if (!details.is_add && details.parent == this) { |
| headers_.erase(std::remove_if(headers_.begin(), headers_.end(), |
| [details](const Header& header) { |
| return header.view == details.child; |
| }), |
| headers_.end()); |
| } else if (details.is_add && details.parent == this && |
| details.child == children().front()) { |
| // We always want padding on the bottom of the scroll contents. |
| // We only want padding on the top of the scroll contents if the first |
| // child is not a header (in that case, the padding is built into the |
| // header). |
| DCHECK_EQ(box_layout_, GetLayoutManager()); |
| box_layout_->set_inside_border_insets( |
| gfx::Insets(details.child->GetID() == VIEW_ID_STICKY_HEADER |
| ? 0 |
| : kMenuSeparatorVerticalPadding, |
| 0, kMenuSeparatorVerticalPadding, 0)); |
| } |
| } |
| |
| private: |
| const int kShadowOffsetY = 2; |
| const int kShadowBlur = 2; |
| |
| // A structure that keeps the original offset of each header between the |
| // calls to Layout() to allow keeping track of which view should be sticky. |
| struct Header { |
| explicit Header(views::View* view) |
| : view(view), natural_offset(view->y()), draw_separator_below(false) {} |
| |
| // A header View that can be decorated as sticky. |
| views::View* view; |
| |
| // Offset from the top of ScrollContentsView to |view|'s original vertical |
| // position. |
| int natural_offset; |
| |
| // True when a separator needs to be painted below the header when another |
| // header is pushing |this| header up. |
| bool draw_separator_below; |
| }; |
| |
| // Adjusts y-position of header rows allowing one or two rows to stick to the |
| // top of the visible viewport. |
| void PositionHeaderRows() { |
| const int scroll_offset = -y(); |
| Header* previous_header = nullptr; |
| for (auto& header : base::Reversed(headers_)) { |
| views::View* header_view = header.view; |
| bool draw_separator_below = false; |
| if (header.natural_offset >= scroll_offset) { |
| previous_header = &header; |
| header_view->SetY(header.natural_offset); |
| } else { |
| if (previous_header && previous_header->view->y() <= |
| scroll_offset + header_view->height()) { |
| // Lower header displacing the header above. |
| draw_separator_below = true; |
| header_view->SetY(previous_header->view->y() - header_view->height()); |
| } else { |
| // A header becomes sticky. |
| header_view->SetY(scroll_offset); |
| header_view->Layout(); |
| header_view->SchedulePaint(); |
| } |
| } |
| if (header.draw_separator_below != draw_separator_below) { |
| header.draw_separator_below = draw_separator_below; |
| delegate_->ShowStickyHeaderSeparator(header_view, draw_separator_below); |
| } |
| if (header.natural_offset < scroll_offset) |
| break; |
| } |
| } |
| |
| // Paints a separator for a header view. The separator can be a horizontal |
| // rule or a horizontal shadow, depending on whether the header is sticking to |
| // the top of the scroll viewport. The return value indicates whether a shadow |
| // was drawn. |
| bool PaintDelineation(const Header& header, const ui::PaintContext& context) { |
| const View* view = header.view; |
| |
| // If the header is where it normally belongs or If the header is pushed by |
| // a header directly below it, draw nothing. |
| if (view->y() == header.natural_offset || header.draw_separator_below) |
| return false; |
| |
| // Otherwise, draw a shadow below. |
| DrawShadow(context, |
| gfx::Rect(0, 0, view->width(), view->bounds().bottom())); |
| return true; |
| } |
| |
| // Draws a drop shadow below |shadowed_area|. |
| void DrawShadow(const ui::PaintContext& context, |
| const gfx::Rect& shadowed_area) { |
| ui::PaintRecorder recorder(context, size()); |
| gfx::Canvas* canvas = recorder.canvas(); |
| cc::PaintFlags flags; |
| gfx::ShadowValues shadow; |
| shadow.emplace_back( |
| gfx::Vector2d(0, kShadowOffsetY), kShadowBlur, |
| AshColorProvider::Get()->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kSeparatorColor)); |
| flags.setLooper(gfx::CreateShadowDrawLooper(shadow)); |
| flags.setAntiAlias(true); |
| canvas->ClipRect(shadowed_area, SkClipOp::kDifference); |
| canvas->DrawRect(shadowed_area, flags); |
| } |
| |
| DetailedViewDelegate* const delegate_; |
| |
| views::BoxLayout* box_layout_ = nullptr; |
| |
| // Header child views that stick to the top of visible viewport when scrolled. |
| std::vector<Header> headers_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ScrollContentsView); |
| }; |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // TrayDetailedView: |
| |
| TrayDetailedView::TrayDetailedView(DetailedViewDelegate* delegate) |
| : delegate_(delegate) { |
| box_layout_ = SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kVertical, |
| delegate->GetInsetsForDetailedView())); |
| SetBackground(views::CreateSolidBackground( |
| delegate_->GetBackgroundColor().value_or(SK_ColorTRANSPARENT))); |
| } |
| |
| TrayDetailedView::~TrayDetailedView() = default; |
| |
| void TrayDetailedView::OnViewClicked(views::View* sender) { |
| HandleViewClicked(sender); |
| } |
| |
| void TrayDetailedView::CreateTitleRow(int string_id) { |
| DCHECK(!tri_view_); |
| |
| tri_view_ = delegate_->CreateTitleRow(string_id); |
| |
| back_button_ = delegate_->CreateBackButton(base::BindRepeating( |
| &TrayDetailedView::TransitionToMainView, base::Unretained(this))); |
| tri_view_->AddView(TriView::Container::START, back_button_); |
| |
| AddChildViewAt(tri_view_, 0); |
| AddChildViewAt(delegate_->CreateTitleSeparator(), kTitleRowSeparatorIndex); |
| |
| CreateExtraTitleRowButtons(); |
| Layout(); |
| } |
| |
| void TrayDetailedView::CreateScrollableList() { |
| DCHECK(!scroller_); |
| auto scroll_content = std::make_unique<ScrollContentsView>(delegate_); |
| scroller_ = AddChildView(std::make_unique<views::ScrollView>()); |
| scroller_->SetDrawOverflowIndicator(delegate_->IsOverflowIndicatorEnabled()); |
| scroll_content_ = scroller_->SetContents(std::move(scroll_content)); |
| // TODO(varkha): Make the sticky rows work with EnableViewPortLayer(). |
| scroller_->SetBackgroundColor(delegate_->GetBackgroundColor()); |
| |
| box_layout_->SetFlexForView(scroller_, 1); |
| } |
| |
| void TrayDetailedView::AddScrollListChild(std::unique_ptr<views::View> child) { |
| scroll_content_->AddChildView(std::move(child)); |
| } |
| |
| HoverHighlightView* TrayDetailedView::AddScrollListItem( |
| const gfx::VectorIcon& icon, |
| const std::u16string& text) { |
| HoverHighlightView* item = delegate_->CreateScrollListItem(this, icon, text); |
| scroll_content_->AddChildView(item); |
| return item; |
| } |
| |
| HoverHighlightView* TrayDetailedView::AddScrollListCheckableItem( |
| const gfx::VectorIcon& icon, |
| const std::u16string& text, |
| bool checked, |
| bool enterprise_managed) { |
| HoverHighlightView* item = AddScrollListItem(icon, text); |
| if (enterprise_managed) { |
| item->SetAccessibleName(l10n_util::GetStringFUTF16( |
| IDS_ASH_ACCESSIBILITY_FEATURE_MANAGED, text)); |
| } |
| TrayPopupUtils::InitializeAsCheckableRow(item, checked, enterprise_managed); |
| return item; |
| } |
| |
| HoverHighlightView* TrayDetailedView::AddScrollListCheckableItem( |
| const std::u16string& text, |
| bool checked, |
| bool enterprise_managed) { |
| return AddScrollListCheckableItem(gfx::kNoneIcon, text, checked, |
| enterprise_managed); |
| } |
| |
| void TrayDetailedView::SetupConnectedScrollListItem(HoverHighlightView* view) { |
| SetupConnectedScrollListItem(view, absl::nullopt /* battery_percentage */); |
| } |
| |
| void TrayDetailedView::SetupConnectedScrollListItem( |
| HoverHighlightView* view, |
| absl::optional<uint8_t> battery_percentage) { |
| DCHECK(view->is_populated()); |
| |
| std::u16string status; |
| |
| if (battery_percentage) { |
| view->SetSubText(l10n_util::GetStringFUTF16( |
| IDS_ASH_STATUS_TRAY_BLUETOOTH_DEVICE_CONNECTED_WITH_BATTERY_LABEL, |
| base::NumberToString16(battery_percentage.value()))); |
| } else { |
| view->SetSubText(l10n_util::GetStringUTF16( |
| IDS_ASH_STATUS_TRAY_NETWORK_STATUS_CONNECTED)); |
| } |
| |
| view->sub_text_label()->SetAutoColorReadabilityEnabled(false); |
| view->sub_text_label()->SetEnabledColor( |
| AshColorProvider::Get()->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kTextColorPositive)); |
| } |
| |
| void TrayDetailedView::SetupConnectingScrollListItem(HoverHighlightView* view) { |
| DCHECK(view->is_populated()); |
| |
| view->SetSubText( |
| l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_NETWORK_STATUS_CONNECTING)); |
| } |
| |
| TriView* TrayDetailedView::AddScrollListSubHeader(const gfx::VectorIcon& icon, |
| int text_id) { |
| TriView* header = TrayPopupUtils::CreateSubHeaderRowView(true); |
| TrayPopupUtils::ConfigureAsStickyHeader(header); |
| |
| auto* color_provider = AshColorProvider::Get(); |
| sub_header_label_ = TrayPopupUtils::CreateDefaultLabel(); |
| sub_header_label_->SetText(l10n_util::GetStringUTF16(text_id)); |
| sub_header_label_->SetEnabledColor(color_provider->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kTextColorPrimary)); |
| TrayPopupUtils::SetLabelFontList(sub_header_label_, |
| TrayPopupUtils::FontStyle::kSubHeader); |
| header->AddView(TriView::Container::CENTER, sub_header_label_); |
| |
| sub_header_image_view_ = TrayPopupUtils::CreateMainImageView(); |
| sub_header_icon_ = &icon; |
| sub_header_image_view_->SetImage(gfx::CreateVectorIcon( |
| icon, color_provider->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kIconColorPrimary))); |
| header->AddView(TriView::Container::START, sub_header_image_view_); |
| |
| scroll_content_->AddChildView(header); |
| return header; |
| } |
| |
| TriView* TrayDetailedView::AddScrollListSubHeader(int text_id) { |
| return AddScrollListSubHeader(gfx::kNoneIcon, text_id); |
| } |
| |
| void TrayDetailedView::Reset() { |
| RemoveAllChildViews(true); |
| scroller_ = nullptr; |
| scroll_content_ = nullptr; |
| progress_bar_ = nullptr; |
| back_button_ = nullptr; |
| tri_view_ = nullptr; |
| } |
| |
| void TrayDetailedView::ShowProgress(double value, bool visible) { |
| DCHECK(tri_view_); |
| if (!progress_bar_) { |
| progress_bar_ = AddChildViewAt( |
| std::make_unique<views::ProgressBar>(kTitleRowProgressBarHeight), |
| kTitleRowSeparatorIndex + 1); |
| progress_bar_->GetViewAccessibility().OverrideName( |
| l10n_util::GetStringUTF16( |
| IDS_ASH_STATUS_TRAY_NETWORK_PROGRESS_ACCESSIBLE_NAME)); |
| progress_bar_->SetVisible(false); |
| progress_bar_->SetForegroundColor( |
| AshColorProvider::Get()->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kIconColorProminent)); |
| } |
| |
| progress_bar_->SetValue(value); |
| progress_bar_->SetVisible(visible); |
| children()[size_t{kTitleRowSeparatorIndex}]->SetVisible(!visible); |
| } |
| |
| views::Button* TrayDetailedView::CreateInfoButton( |
| views::Button::PressedCallback callback, |
| int info_accessible_name_id) { |
| return delegate_->CreateInfoButton(std::move(callback), |
| info_accessible_name_id); |
| } |
| |
| views::Button* TrayDetailedView::CreateSettingsButton( |
| views::Button::PressedCallback callback, |
| int setting_accessible_name_id) { |
| return delegate_->CreateSettingsButton(std::move(callback), |
| setting_accessible_name_id); |
| } |
| |
| views::Button* TrayDetailedView::CreateHelpButton( |
| views::Button::PressedCallback callback) { |
| return delegate_->CreateHelpButton(std::move(callback)); |
| } |
| |
| views::Separator* TrayDetailedView::CreateListSubHeaderSeparator() { |
| return delegate_->CreateListSubHeaderSeparator(); |
| } |
| |
| void TrayDetailedView::HandleViewClicked(views::View* view) { |
| NOTREACHED(); |
| } |
| |
| void TrayDetailedView::CreateExtraTitleRowButtons() {} |
| |
| void TrayDetailedView::TransitionToMainView() { |
| delegate_->TransitionToMainView(back_button_ && back_button_->HasFocus()); |
| } |
| |
| void TrayDetailedView::CloseBubble() { |
| // widget may be null in tests, in this case we do not need to do anything. |
| views::Widget* widget = GetWidget(); |
| if (!widget) |
| return; |
| // Don't close again if we're already closing. |
| if (widget->IsClosed()) |
| return; |
| delegate_->CloseBubble(); |
| } |
| |
| void TrayDetailedView::Layout() { |
| views::View::Layout(); |
| if (scroller_ && !scroller_->is_bounded()) |
| scroller_->ClipHeightTo(0, scroller_->height()); |
| } |
| |
| int TrayDetailedView::GetHeightForWidth(int width) const { |
| if (bounds().IsEmpty()) |
| return views::View::GetHeightForWidth(width); |
| |
| // The height of the bubble that contains this detailed view is set to |
| // the preferred height of the default view, and that determines the |
| // initial height of |this|. Always request to stay the same height. |
| return height(); |
| } |
| |
| const char* TrayDetailedView::GetClassName() const { |
| return "TrayDetailedView"; |
| } |
| |
| void TrayDetailedView::OnThemeChanged() { |
| views::View::OnThemeChanged(); |
| delegate_->UpdateColors(); |
| |
| auto* color_provider = AshColorProvider::Get(); |
| if (sub_header_label_) { |
| sub_header_label_->SetEnabledColor(color_provider->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kTextColorPrimary)); |
| } |
| if (sub_header_image_view_) { |
| sub_header_image_view_->SetImage(gfx::CreateVectorIcon( |
| *sub_header_icon_, |
| color_provider->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kIconColorPrimary))); |
| } |
| } |
| |
| } // namespace ash |