| // 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_details_view.h" |
| |
| #include "ash/ash_view_ids.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/system/tray/hover_highlight_view.h" |
| #include "ash/system/tray/system_menu_button.h" |
| #include "ash/system/tray/system_tray.h" |
| #include "ash/system/tray/system_tray_item.h" |
| #include "ash/system/tray/tray_constants.h" |
| #include "ash/system/tray/tray_popup_item_style.h" |
| #include "ash/system/tray/tray_popup_utils.h" |
| #include "ash/system/tray/tri_view.h" |
| #include "base/containers/adapters.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/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/native_theme/native_theme.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" |
| |
| 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 set_id(VIEW_ID_STICKY_HEADER). |
| class ScrollContentsView : public views::View { |
| public: |
| ScrollContentsView() { |
| box_layout_ = SetLayoutManager( |
| std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical)); |
| } |
| ~ScrollContentsView() override = default; |
| |
| protected: |
| // views::View: |
| void OnBoundsChanged(const gfx::Rect& previous_bounds) override { |
| PositionHeaderRows(); |
| } |
| |
| void PaintChildren(const views::PaintInfo& paint_info) override { |
| views::View::PaintChildren(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() - kSeparatorWidth)); |
| } |
| |
| void Layout() override { |
| views::View::Layout(); |
| headers_.clear(); |
| for (int i = 0; i < child_count(); ++i) { |
| views::View* view = child_at(i); |
| if (view->id() == VIEW_ID_STICKY_HEADER) |
| headers_.emplace_back(view); |
| } |
| PositionHeaderRows(); |
| } |
| |
| View::Views GetChildrenInZOrder() override { |
| View::Views children; |
| // Iterate over regular children and later over the sticky headers to keep |
| // the sticky headers above in Z-order. |
| for (int i = 0; i < child_count(); ++i) { |
| if (child_at(i)->id() != VIEW_ID_STICKY_HEADER) |
| children.push_back(child_at(i)); |
| } |
| for (int i = 0; i < child_count(); ++i) { |
| if (child_at(i)->id() == VIEW_ID_STICKY_HEADER) |
| children.push_back(child_at(i)); |
| } |
| DCHECK_EQ(child_count(), static_cast<int>(children.size())); |
| return children; |
| } |
| |
| void ViewHierarchyChanged( |
| const 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 == child_at(0)) { |
| // 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->id() == 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; |
| TrayPopupUtils::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, |
| kMenuSeparatorColor); |
| flags.setLooper(gfx::CreateShadowDrawLooper(shadow)); |
| flags.setAntiAlias(true); |
| canvas->ClipRect(shadowed_area, SkClipOp::kDifference); |
| canvas->DrawRect(shadowed_area, flags); |
| } |
| |
| 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); |
| }; |
| |
| // Constants for the title row. |
| const int kTitleRowVerticalPadding = 4; |
| const int kTitleRowProgressBarHeight = 2; |
| const int kTitleRowPaddingTop = kTitleRowVerticalPadding; |
| const int kTitleRowPaddingBottom = |
| kTitleRowVerticalPadding - kTitleRowProgressBarHeight; |
| |
| } // namespace |
| |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // TrayDetailsView: |
| |
| TrayDetailsView::TrayDetailsView(SystemTrayItem* owner) |
| : owner_(owner), |
| box_layout_(nullptr), |
| scroller_(nullptr), |
| scroll_content_(nullptr), |
| progress_bar_(nullptr), |
| tri_view_(nullptr), |
| back_button_(nullptr) { |
| box_layout_ = SetLayoutManager( |
| std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical)); |
| SetBackground(views::CreateThemedSolidBackground( |
| this, ui::NativeTheme::kColorId_BubbleBackground)); |
| } |
| |
| TrayDetailsView::~TrayDetailsView() = default; |
| |
| void TrayDetailsView::OnViewClicked(views::View* sender) { |
| HandleViewClicked(sender); |
| } |
| |
| void TrayDetailsView::ButtonPressed(views::Button* sender, |
| const ui::Event& event) { |
| if (sender == back_button_) { |
| TransitionToDefaultView(); |
| return; |
| } |
| |
| HandleButtonPressed(sender, event); |
| } |
| |
| void TrayDetailsView::CreateTitleRow(int string_id) { |
| DCHECK(!tri_view_); |
| |
| tri_view_ = TrayPopupUtils::CreateDefaultRowView(); |
| |
| back_button_ = CreateBackButton(); |
| tri_view_->AddView(TriView::Container::START, back_button_); |
| |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| auto* label = TrayPopupUtils::CreateDefaultLabel(); |
| label->SetText(rb.GetLocalizedString(string_id)); |
| TrayPopupItemStyle style(TrayPopupItemStyle::FontStyle::TITLE); |
| style.SetupLabel(label); |
| tri_view_->AddView(TriView::Container::CENTER, label); |
| |
| tri_view_->SetContainerVisible(TriView::Container::END, false); |
| |
| tri_view_->SetBorder(views::CreateEmptyBorder(kTitleRowPaddingTop, 0, |
| kTitleRowPaddingBottom, 0)); |
| AddChildViewAt(tri_view_, 0); |
| views::Separator* separator = new views::Separator(); |
| separator->SetColor(kMenuSeparatorColor); |
| separator->SetBorder(views::CreateEmptyBorder( |
| kTitleRowProgressBarHeight - views::Separator::kThickness, 0, 0, 0)); |
| AddChildViewAt(separator, kTitleRowSeparatorIndex); |
| |
| CreateExtraTitleRowButtons(); |
| Layout(); |
| } |
| |
| void TrayDetailsView::CreateScrollableList() { |
| DCHECK(!scroller_); |
| scroll_content_ = new ScrollContentsView(); |
| scroller_ = new views::ScrollView; |
| scroller_->SetContents(scroll_content_); |
| // TODO(varkha): Make the sticky rows work with EnableViewPortLayer(). |
| scroller_->SetBackgroundThemeColorId( |
| ui::NativeTheme::kColorId_BubbleBackground); |
| |
| AddChildView(scroller_); |
| box_layout_->SetFlexForView(scroller_, 1); |
| } |
| |
| HoverHighlightView* TrayDetailsView::AddScrollListItem( |
| const gfx::VectorIcon& icon, |
| const base::string16& text) { |
| HoverHighlightView* item = new HoverHighlightView(this); |
| if (icon.is_empty()) |
| item->AddLabelRow(text); |
| else |
| item->AddIconAndLabel(gfx::CreateVectorIcon(icon, kMenuIconColor), text); |
| scroll_content_->AddChildView(item); |
| return item; |
| } |
| |
| HoverHighlightView* TrayDetailsView::AddScrollListCheckableItem( |
| const gfx::VectorIcon& icon, |
| const base::string16& text, |
| bool checked) { |
| HoverHighlightView* item = AddScrollListItem(icon, text); |
| TrayPopupUtils::InitializeAsCheckableRow(item, checked); |
| return item; |
| } |
| |
| HoverHighlightView* TrayDetailsView::AddScrollListCheckableItem( |
| const base::string16& text, |
| bool checked) { |
| return AddScrollListCheckableItem(gfx::kNoneIcon, text, checked); |
| } |
| |
| void TrayDetailsView::SetupConnectedScrollListItem(HoverHighlightView* view) { |
| DCHECK(view->is_populated()); |
| |
| view->SetSubText( |
| l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_NETWORK_STATUS_CONNECTED)); |
| TrayPopupItemStyle style(TrayPopupItemStyle::FontStyle::CAPTION); |
| style.set_color_style(TrayPopupItemStyle::ColorStyle::CONNECTED); |
| style.SetupLabel(view->sub_text_label()); |
| } |
| |
| void TrayDetailsView::SetupConnectingScrollListItem(HoverHighlightView* view) { |
| DCHECK(view->is_populated()); |
| |
| view->SetSubText( |
| l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_NETWORK_STATUS_CONNECTING)); |
| } |
| |
| TriView* TrayDetailsView::AddScrollListSubHeader(const gfx::VectorIcon& icon, |
| int text_id) { |
| TriView* header = TrayPopupUtils::CreateSubHeaderRowView(!icon.is_empty()); |
| TrayPopupUtils::ConfigureAsStickyHeader(header); |
| |
| views::Label* label = TrayPopupUtils::CreateDefaultLabel(); |
| label->SetText(l10n_util::GetStringUTF16(text_id)); |
| TrayPopupItemStyle style(TrayPopupItemStyle::FontStyle::SUB_HEADER); |
| style.SetupLabel(label); |
| header->AddView(TriView::Container::CENTER, label); |
| |
| if (!icon.is_empty()) { |
| views::ImageView* image_view = TrayPopupUtils::CreateMainImageView(); |
| image_view->SetImage(gfx::CreateVectorIcon( |
| icon, GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_ProminentButtonColor))); |
| header->AddView(TriView::Container::START, image_view); |
| } |
| |
| scroll_content_->AddChildView(header); |
| return header; |
| } |
| |
| TriView* TrayDetailsView::AddScrollListSubHeader(int text_id) { |
| return AddScrollListSubHeader(gfx::kNoneIcon, text_id); |
| } |
| |
| void TrayDetailsView::Reset() { |
| RemoveAllChildViews(true); |
| scroller_ = nullptr; |
| scroll_content_ = nullptr; |
| progress_bar_ = nullptr; |
| back_button_ = nullptr; |
| tri_view_ = nullptr; |
| } |
| |
| void TrayDetailsView::ShowProgress(double value, bool visible) { |
| DCHECK(tri_view_); |
| if (!progress_bar_) { |
| progress_bar_ = new views::ProgressBar(kTitleRowProgressBarHeight); |
| progress_bar_->SetVisible(false); |
| AddChildViewAt(progress_bar_, kTitleRowSeparatorIndex + 1); |
| } |
| |
| progress_bar_->SetValue(value); |
| progress_bar_->SetVisible(visible); |
| child_at(kTitleRowSeparatorIndex)->SetVisible(!visible); |
| } |
| |
| views::Button* TrayDetailsView::CreateSettingsButton( |
| int setting_accessible_name_id) { |
| SystemMenuButton* button = |
| new SystemMenuButton(this, TrayPopupInkDropStyle::HOST_CENTERED, |
| kSystemMenuSettingsIcon, setting_accessible_name_id); |
| if (!TrayPopupUtils::CanOpenWebUISettings()) |
| button->SetEnabled(false); |
| return button; |
| } |
| |
| views::Button* TrayDetailsView::CreateHelpButton() { |
| SystemMenuButton* button = |
| new SystemMenuButton(this, TrayPopupInkDropStyle::HOST_CENTERED, |
| kSystemMenuHelpIcon, IDS_ASH_STATUS_TRAY_HELP); |
| // Help opens a web page, so treat it like Web UI settings. |
| if (!TrayPopupUtils::CanOpenWebUISettings()) |
| button->SetEnabled(false); |
| return button; |
| } |
| |
| void TrayDetailsView::HandleViewClicked(views::View* view) { |
| NOTREACHED(); |
| } |
| |
| void TrayDetailsView::HandleButtonPressed(views::Button* sender, |
| const ui::Event& event) { |
| NOTREACHED(); |
| } |
| |
| void TrayDetailsView::CreateExtraTitleRowButtons() {} |
| |
| void TrayDetailsView::TransitionToDefaultView() { |
| if (back_button_ && back_button_->HasFocus()) |
| owner_->set_restore_focus(true); |
| |
| transition_delay_timer_.Start( |
| FROM_HERE, |
| base::TimeDelta::FromMilliseconds(kTrayDetailedViewTransitionDelayMs), |
| this, &TrayDetailsView::DoTransitionToDefaultView); |
| } |
| |
| void TrayDetailsView::DoTransitionToDefaultView() { |
| // Cache pointer to owner in this function scope. TrayDetailsView will be |
| // deleted after called ShowDefaultView. |
| SystemTrayItem* owner = owner_; |
| owner->system_tray()->ShowDefaultView(BUBBLE_USE_EXISTING, |
| false /* show_by_click */); |
| owner->set_restore_focus(false); |
| } |
| |
| views::Button* TrayDetailsView::CreateBackButton() { |
| SystemMenuButton* button = new SystemMenuButton( |
| this, TrayPopupInkDropStyle::HOST_CENTERED, kSystemMenuArrowBackIcon, |
| IDS_ASH_STATUS_TRAY_PREVIOUS_MENU); |
| return button; |
| } |
| |
| void TrayDetailsView::Layout() { |
| views::View::Layout(); |
| if (scroller_ && !scroller_->is_bounded()) |
| scroller_->ClipHeightTo(0, scroller_->height()); |
| } |
| |
| int TrayDetailsView::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(); |
| } |
| |
| } // namespace ash |