| // Copyright 2018 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/message_center/unified_message_center_view.h" |
| |
| #include <algorithm> |
| #include <memory> |
| |
| #include "ash/public/cpp/ash_features.h" |
| #include "ash/session/session_controller.h" |
| #include "ash/shell.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/system/message_center/ash_message_center_lock_screen_controller.h" |
| #include "ash/system/message_center/message_center_scroll_bar.h" |
| #include "ash/system/message_center/unified_message_list_view.h" |
| #include "ash/system/tray/tray_constants.h" |
| #include "ash/system/tray/tray_popup_utils.h" |
| #include "ash/system/unified/sign_out_button.h" |
| #include "ash/system/unified/unified_system_tray_model.h" |
| #include "ash/system/unified/unified_system_tray_view.h" |
| #include "base/metrics/user_metrics.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/message_center/message_center.h" |
| #include "ui/message_center/public/cpp/message_center_constants.h" |
| #include "ui/message_center/views/message_view.h" |
| #include "ui/views/animation/flood_fill_ink_drop_ripple.h" |
| #include "ui/views/animation/ink_drop_highlight.h" |
| #include "ui/views/animation/ink_drop_impl.h" |
| #include "ui/views/animation/ink_drop_mask.h" |
| #include "ui/views/background.h" |
| #include "ui/views/controls/button/label_button.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| enum ClearAllButtonTag { |
| kStackingBarClearAllButtonTag, |
| kBottomClearAllButtonTag, |
| }; |
| |
| constexpr int kClearAllButtonRowHeight = 3 * kUnifiedNotificationCenterSpacing; |
| |
| class ScrollerContentsView : public views::View { |
| public: |
| ScrollerContentsView(UnifiedMessageListView* message_list_view, |
| views::ButtonListener* listener) { |
| auto* contents_layout = SetLayoutManager( |
| std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical)); |
| contents_layout->set_cross_axis_alignment( |
| views::BoxLayout::CROSS_AXIS_ALIGNMENT_STRETCH); |
| AddChildView(message_list_view); |
| |
| views::View* button_container = new views::View; |
| auto* button_layout = |
| button_container->SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::kHorizontal, |
| gfx::Insets(kUnifiedNotificationCenterSpacing), 0)); |
| button_layout->set_main_axis_alignment( |
| views::BoxLayout::MAIN_AXIS_ALIGNMENT_END); |
| |
| auto* clear_all_button = new RoundedLabelButton( |
| listener, l10n_util::GetStringUTF16( |
| IDS_ASH_MESSAGE_CENTER_CLEAR_ALL_BUTTON_LABEL)); |
| clear_all_button->SetTooltipText(l10n_util::GetStringUTF16( |
| IDS_ASH_MESSAGE_CENTER_CLEAR_ALL_BUTTON_TOOLTIP)); |
| clear_all_button->set_tag(kBottomClearAllButtonTag); |
| button_container->AddChildView(clear_all_button); |
| AddChildView(button_container); |
| } |
| |
| ~ScrollerContentsView() override = default; |
| |
| // views::View: |
| void ChildPreferredSizeChanged(views::View* view) override { |
| PreferredSizeChanged(); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(ScrollerContentsView); |
| }; |
| |
| // The "Clear all" button in the stacking notification bar. |
| class StackingBarClearAllButton : public views::LabelButton { |
| public: |
| StackingBarClearAllButton(views::ButtonListener* listener, |
| const base::string16& text) |
| : views::LabelButton(listener, text) { |
| set_tag(kStackingBarClearAllButtonTag); |
| SetEnabledTextColors(kUnifiedMenuButtonColorActive); |
| SetHorizontalAlignment(gfx::ALIGN_CENTER); |
| SetBorder(views::CreateEmptyBorder(gfx::Insets())); |
| label()->SetSubpixelRenderingEnabled(false); |
| label()->SetFontList(views::Label::GetDefaultFontList().Derive( |
| 1, gfx::Font::NORMAL, gfx::Font::Weight::MEDIUM)); |
| TrayPopupUtils::ConfigureTrayPopupButton(this); |
| } |
| |
| ~StackingBarClearAllButton() override = default; |
| |
| // views::LabelButton: |
| gfx::Size CalculatePreferredSize() const override { |
| return gfx::Size(label()->GetPreferredSize().width() + |
| kStackingNotificationClearAllButtonPadding.width(), |
| label()->GetPreferredSize().height() + |
| kStackingNotificationClearAllButtonPadding.height()); |
| } |
| |
| int GetHeightForWidth(int width) const override { |
| return label()->GetPreferredSize().height() + |
| kStackingNotificationClearAllButtonPadding.height(); |
| } |
| |
| void PaintButtonContents(gfx::Canvas* canvas) override { |
| views::LabelButton::PaintButtonContents(canvas); |
| } |
| |
| std::unique_ptr<views::InkDrop> CreateInkDrop() override { |
| auto ink_drop = TrayPopupUtils::CreateInkDrop(this); |
| ink_drop->SetShowHighlightOnFocus(true); |
| ink_drop->SetShowHighlightOnHover(true); |
| return ink_drop; |
| } |
| |
| std::unique_ptr<views::InkDropRipple> CreateInkDropRipple() const override { |
| return TrayPopupUtils::CreateInkDropRipple( |
| TrayPopupInkDropStyle::FILL_BOUNDS, this, |
| GetInkDropCenterBasedOnLastEvent(), SK_ColorBLACK); |
| } |
| |
| std::unique_ptr<views::InkDropHighlight> CreateInkDropHighlight() |
| const override { |
| return TrayPopupUtils::CreateInkDropHighlight( |
| TrayPopupInkDropStyle::FILL_BOUNDS, this, SK_ColorBLACK); |
| } |
| |
| std::unique_ptr<views::InkDropMask> CreateInkDropMask() const override { |
| SkScalar top_radius = SkIntToScalar(kUnifiedTrayCornerRadius); |
| SkScalar radii[8] = {0, 0, top_radius, top_radius, 0, 0, 0, 0}; |
| SkPath path; |
| path.addRoundRect(gfx::RectToSkRect(GetContentsBounds()), radii); |
| |
| return std::make_unique<views::PathInkDropMask>(size(), path); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(StackingBarClearAllButton); |
| }; |
| |
| int GetStackingNotificationCounterHeight() { |
| return features::IsNotificationStackingBarRedesignEnabled() |
| ? kStackingNotificationCounterWithClearAllHeight |
| : kStackingNotificationCounterHeight; |
| } |
| |
| } // namespace |
| |
| StackingNotificationCounterView::StackingNotificationCounterView( |
| views::ButtonListener* listener) { |
| SetVisible(false); |
| |
| if (!features::IsNotificationStackingBarRedesignEnabled()) |
| return; |
| |
| auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::kHorizontal, |
| gfx::Insets(0, kStackingNotificationClearAllButtonPadding.left(), 0, 0), |
| 0)); |
| layout->set_cross_axis_alignment( |
| views::BoxLayout::CROSS_AXIS_ALIGNMENT_STRETCH); |
| |
| count_label_ = new views::Label(); |
| count_label_->SetEnabledColor(kStackingNotificationCounterLabelColor); |
| count_label_->SetFontList(views::Label::GetDefaultFontList().Derive( |
| 1, gfx::Font::NORMAL, gfx::Font::Weight::MEDIUM)); |
| AddChildView(count_label_); |
| |
| views::View* spacer = new views::View; |
| AddChildView(spacer); |
| layout->SetFlexForView(spacer, 1); |
| |
| clear_all_button_ = new StackingBarClearAllButton( |
| listener, |
| l10n_util::GetStringUTF16(IDS_ASH_MESSAGE_CENTER_CLEAR_ALL_BUTTON_LABEL)); |
| clear_all_button_->SetTooltipText(l10n_util::GetStringUTF16( |
| IDS_ASH_MESSAGE_CENTER_CLEAR_ALL_BUTTON_TOOLTIP)); |
| AddChildView(clear_all_button_); |
| } |
| |
| StackingNotificationCounterView::~StackingNotificationCounterView() = default; |
| |
| bool StackingNotificationCounterView::SetCount(int total_notification_count, |
| int stacked_notification_count) { |
| if (total_notification_count == total_notification_count_ && |
| stacked_notification_count == stacked_notification_count_) |
| return false; |
| |
| total_notification_count_ = total_notification_count; |
| stacked_notification_count_ = stacked_notification_count; |
| |
| if (features::IsNotificationStackingBarRedesignEnabled()) { |
| SetVisible(total_notification_count_ > 1); |
| |
| auto tooltip = l10n_util::GetStringFUTF16Int( |
| IDS_ASH_MESSAGE_CENTER_STACKING_BAR_CLEAR_ALL_BUTTON_TOOLTIP, |
| total_notification_count_); |
| clear_all_button_->SetTooltipText(tooltip); |
| clear_all_button_->SetAccessibleName(tooltip); |
| |
| if (stacked_notification_count_ > 0) { |
| count_label_->SetText(l10n_util::GetStringFUTF16Int( |
| IDS_ASH_MESSAGE_CENTER_HIDDEN_NOTIFICATION_COUNT_LABEL, |
| stacked_notification_count_)); |
| count_label_->SetVisible(true); |
| } else { |
| count_label_->SetVisible(false); |
| } |
| } else { |
| SetVisible(stacked_notification_count_ > 0); |
| } |
| |
| SchedulePaint(); |
| return true; |
| } |
| |
| void StackingNotificationCounterView::OnPaint(gfx::Canvas* canvas) { |
| cc::PaintFlags flags; |
| flags.setColor(message_center::kNotificationBackgroundColor); |
| flags.setStyle(cc::PaintFlags::kFill_Style); |
| flags.setAntiAlias(true); |
| |
| SkPath background_path; |
| SkScalar top_radius = SkIntToScalar(kUnifiedTrayCornerRadius); |
| SkScalar radii[8] = {top_radius, top_radius, top_radius, top_radius, |
| 0, 0, 0, 0}; |
| |
| gfx::Rect bounds = GetLocalBounds(); |
| background_path.addRoundRect(gfx::RectToSkRect(bounds), radii); |
| canvas->DrawPath(background_path, flags); |
| |
| // We draw a border here than use a views::Border so the ink drop highlight |
| // of the clear all button overlays the border. |
| canvas->DrawSharpLine( |
| gfx::PointF(bounds.bottom_left() - gfx::Vector2d(0, 1)), |
| gfx::PointF(bounds.bottom_right() - gfx::Vector2d(0, 1)), |
| kStackingNotificationCounterBorderColor); |
| |
| if (features::IsNotificationStackingBarRedesignEnabled()) |
| return; |
| |
| // Draw the hidden notification dots for the the old UI. |
| int x = kStackingNotificationCounterStartX; |
| const int y = kStackingNotificationCounterHeight / 2; |
| int stacking_count = |
| std::min(stacked_notification_count_, kStackingNotificationCounterMax); |
| flags.setColor(kStackingNotificationCounterColor); |
| for (int i = 0; i < stacking_count; ++i) { |
| canvas->DrawCircle(gfx::Point(x, y), kStackingNotificationCounterRadius, |
| flags); |
| x += kStackingNotificationCounterDistanceX; |
| } |
| |
| views::View::OnPaint(canvas); |
| } |
| |
| UnifiedMessageCenterView::UnifiedMessageCenterView( |
| UnifiedSystemTrayView* parent, |
| UnifiedSystemTrayModel* model) |
| : parent_(parent), |
| model_(model), |
| stacking_counter_(new StackingNotificationCounterView(this)), |
| scroll_bar_(new MessageCenterScrollBar(this)), |
| scroller_(new views::ScrollView()), |
| message_list_view_(new UnifiedMessageListView(this, model)), |
| last_scroll_position_from_bottom_(kClearAllButtonRowHeight) { |
| message_list_view_->Init(); |
| |
| AddChildView(stacking_counter_); |
| |
| // Need to set the transparent background explicitly, since ScrollView has |
| // set the default opaque background color. |
| scroller_->SetContents( |
| std::make_unique<ScrollerContentsView>(message_list_view_, this)); |
| scroller_->SetBackgroundColor(SK_ColorTRANSPARENT); |
| scroller_->SetVerticalScrollBar(scroll_bar_); |
| scroller_->set_draw_overflow_indicator(false); |
| AddChildView(scroller_); |
| } |
| |
| UnifiedMessageCenterView::~UnifiedMessageCenterView() { |
| model_->set_notification_target_mode( |
| UnifiedSystemTrayModel::NotificationTargetMode::LAST_NOTIFICATION); |
| |
| RemovedFromWidget(); |
| } |
| |
| void UnifiedMessageCenterView::SetMaxHeight(int max_height) { |
| scroller_->ClipHeightTo(0, max_height); |
| } |
| |
| void UnifiedMessageCenterView::SetAvailableHeight(int available_height) { |
| available_height_ = available_height; |
| UpdateVisibility(); |
| } |
| |
| void UnifiedMessageCenterView::ListPreferredSizeChanged() { |
| UpdateVisibility(); |
| PreferredSizeChanged(); |
| Layout(); |
| |
| if (GetWidget() && !GetWidget()->IsClosed()) |
| GetWidget()->SynthesizeMouseMoveEvent(); |
| } |
| |
| void UnifiedMessageCenterView::ConfigureMessageView( |
| message_center::MessageView* message_view) { |
| message_view->set_scroller(scroller_); |
| } |
| |
| void UnifiedMessageCenterView::AddedToWidget() { |
| focus_manager_ = GetFocusManager(); |
| if (focus_manager_) |
| focus_manager_->AddFocusChangeListener(this); |
| } |
| |
| void UnifiedMessageCenterView::RemovedFromWidget() { |
| if (!focus_manager_) |
| return; |
| focus_manager_->RemoveFocusChangeListener(this); |
| focus_manager_ = nullptr; |
| } |
| |
| void UnifiedMessageCenterView::Layout() { |
| stacking_counter_->SetCount(message_list_view_->GetTotalNotificationCount(), |
| GetStackedNotificationCount()); |
| if (stacking_counter_->visible()) { |
| gfx::Rect counter_bounds(GetContentsBounds()); |
| counter_bounds.set_height(GetStackingNotificationCounterHeight()); |
| stacking_counter_->SetBoundsRect(counter_bounds); |
| |
| gfx::Rect scroller_bounds(GetContentsBounds()); |
| scroller_bounds.Inset( |
| gfx::Insets(GetStackingNotificationCounterHeight(), 0, 0, 0)); |
| scroller_->SetBoundsRect(scroller_bounds); |
| } else { |
| scroller_->SetBoundsRect(GetContentsBounds()); |
| } |
| |
| ScrollToTarget(); |
| NotifyRectBelowScroll(); |
| } |
| |
| gfx::Size UnifiedMessageCenterView::CalculatePreferredSize() const { |
| gfx::Size preferred_size = scroller_->GetPreferredSize(); |
| |
| if (stacking_counter_->visible()) { |
| preferred_size.set_height(preferred_size.height() + |
| GetStackingNotificationCounterHeight()); |
| } |
| |
| // Hide Clear All button at the buttom from initial viewport. |
| preferred_size.set_height(preferred_size.height() - kClearAllButtonRowHeight); |
| return preferred_size; |
| } |
| |
| void UnifiedMessageCenterView::OnMessageCenterScrolled() { |
| last_scroll_position_from_bottom_ = |
| scroll_bar_->GetMaxPosition() - scroller_->GetVisibleRect().y(); |
| |
| // Reset the target if user scrolls the list manually. |
| model_->set_notification_target_mode( |
| UnifiedSystemTrayModel::NotificationTargetMode::LAST_POSITION); |
| |
| bool was_count_updated = stacking_counter_->SetCount( |
| message_list_view_->GetTotalNotificationCount(), |
| GetStackedNotificationCount()); |
| if (was_count_updated) { |
| const int previous_y = scroller_->y(); |
| Layout(); |
| // Adjust scroll position when counter visibility is changed so that |
| // on-screen position of notification list does not change. |
| scroll_bar_->ScrollByContentsOffset(previous_y - scroller_->y()); |
| } |
| |
| NotifyRectBelowScroll(); |
| } |
| |
| void UnifiedMessageCenterView::ButtonPressed(views::Button* sender, |
| const ui::Event& event) { |
| if (sender) { |
| switch (sender->tag()) { |
| case kStackingBarClearAllButtonTag: |
| base::RecordAction(base::UserMetricsAction( |
| "StatusArea_Notifications_StackingBarClearAll")); |
| break; |
| case kBottomClearAllButtonTag: |
| base::RecordAction( |
| base::UserMetricsAction("StatusArea_Notifications_ClearAll")); |
| break; |
| } |
| } |
| |
| message_list_view_->ClearAllWithAnimation(); |
| } |
| |
| void UnifiedMessageCenterView::OnWillChangeFocus(views::View* before, |
| views::View* now) {} |
| |
| void UnifiedMessageCenterView::OnDidChangeFocus(views::View* before, |
| views::View* now) { |
| if (message_list_view_->is_deleting_removed_notifications()) |
| return; |
| |
| OnMessageCenterScrolled(); |
| } |
| |
| void UnifiedMessageCenterView::SetNotificationRectBelowScroll( |
| const gfx::Rect& rect_below_scroll) { |
| parent_->SetNotificationRectBelowScroll(rect_below_scroll); |
| } |
| |
| void UnifiedMessageCenterView::UpdateVisibility() { |
| SessionController* session_controller = Shell::Get()->session_controller(); |
| SetVisible(available_height_ >= kUnifiedNotificationMinimumHeight && |
| message_list_view_->GetPreferredSize().height() > 0 && |
| session_controller->ShouldShowNotificationTray() && |
| (!session_controller->IsScreenLocked() || |
| AshMessageCenterLockScreenController::IsEnabled())); |
| |
| // When notification list went invisible, the last notification should be |
| // targeted next time. |
| if (!visible()) { |
| model_->set_notification_target_mode( |
| UnifiedSystemTrayModel::NotificationTargetMode::LAST_NOTIFICATION); |
| NotifyRectBelowScroll(); |
| } |
| } |
| |
| void UnifiedMessageCenterView::ScrollToTarget() { |
| // Following logic doesn't work when the view is invisible, because it uses |
| // the height of |scroller_|. |
| if (!visible()) |
| return; |
| |
| auto target_mode = model_->notification_target_mode(); |
| |
| // Notification views may be deleted during an animation, so wait until it |
| // finishes before scrolling to a new target (see crbug.com/954001). |
| if (message_list_view_->IsAnimating()) |
| target_mode = UnifiedSystemTrayModel::NotificationTargetMode::LAST_POSITION; |
| |
| int position; |
| switch (target_mode) { |
| case UnifiedSystemTrayModel::NotificationTargetMode::LAST_POSITION: |
| // Restore the previous scrolled position with matching the distance from |
| // the bottom. |
| position = |
| scroll_bar_->GetMaxPosition() - last_scroll_position_from_bottom_; |
| break; |
| case UnifiedSystemTrayModel::NotificationTargetMode::NOTIFICATION_ID: |
| FALLTHROUGH; |
| case UnifiedSystemTrayModel::NotificationTargetMode::LAST_NOTIFICATION: { |
| const gfx::Rect& target_rect = |
| (model_->notification_target_mode() == |
| UnifiedSystemTrayModel::NotificationTargetMode::NOTIFICATION_ID) |
| ? message_list_view_->GetNotificationBounds( |
| model_->notification_target_id()) |
| : message_list_view_->GetLastNotificationBounds(); |
| |
| const int last_notification_offset = target_rect.height() - |
| scroller_->height() + |
| kUnifiedNotificationCenterSpacing; |
| if (last_notification_offset > 0) { |
| // If the target notification is taller than |scroller_|, we should |
| // align the top of the notification with the top of |scroller_|. |
| position = target_rect.y(); |
| } else { |
| // Otherwise, we align the bottom of the notification with the bottom of |
| // |scroller_|; |
| position = target_rect.bottom() - scroller_->height(); |
| |
| if (model_->notification_target_mode() == |
| UnifiedSystemTrayModel::NotificationTargetMode::LAST_NOTIFICATION) { |
| position += kUnifiedNotificationCenterSpacing; |
| } |
| } |
| } |
| } |
| |
| scroller_->ScrollToPosition(scroll_bar_, position); |
| last_scroll_position_from_bottom_ = |
| scroll_bar_->GetMaxPosition() - scroller_->GetVisibleRect().y(); |
| } |
| |
| int UnifiedMessageCenterView::GetStackedNotificationCount() const { |
| // CountNotificationsAboveY() only works after SetBoundsRect() is called at |
| // least once. |
| if (scroller_->bounds().IsEmpty()) |
| scroller_->SetBoundsRect(GetContentsBounds()); |
| |
| // Consistently use the y offset below the stacked notification bar in the |
| // UnifiedMessageCenterView to count number of hidden notifications. |
| const int y_offset = scroller_->GetVisibleRect().y() - scroller_->y() + |
| GetStackingNotificationCounterHeight(); |
| return message_list_view_->CountNotificationsAboveY(y_offset); |
| } |
| |
| void UnifiedMessageCenterView::NotifyRectBelowScroll() { |
| // If the message center is hidden, make sure rounded corners are not drawn. |
| if (!visible()) { |
| SetNotificationRectBelowScroll(gfx::Rect()); |
| return; |
| } |
| |
| gfx::Rect rect_below_scroll; |
| rect_below_scroll.set_height( |
| std::max(0, message_list_view_->GetLastNotificationBounds().bottom() - |
| scroller_->GetVisibleRect().bottom())); |
| |
| gfx::Rect notification_bounds = |
| message_list_view_->GetNotificationBoundsBelowY( |
| scroller_->GetVisibleRect().bottom()); |
| rect_below_scroll.set_x(notification_bounds.x()); |
| rect_below_scroll.set_width(notification_bounds.width()); |
| |
| SetNotificationRectBelowScroll(rect_below_scroll); |
| } |
| |
| } // namespace ash |