1 // Copyright (c) 2015 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.
5 #include "base/command_line.h"
6 #include "ui/gfx/animation/slide_animation.h"
7 #include "ui/message_center/message_center_style.h"
8 #include "ui/message_center/message_center_switches.h"
9 #include "ui/message_center/views/message_center_view.h"
10 #include "ui/message_center/views/message_list_view.h"
11 #include "ui/message_center/views/message_view.h"
12 #include "ui/views/animation/bounds_animator.h"
13 #include "ui/views/background.h"
14 #include "ui/views/border.h"
15 #include "ui/views/layout/box_layout.h"
16 #include "ui/views/widget/widget.h"
18 namespace message_center
{
21 const int kAnimateClearingNextNotificationDelayMS
= 40;
24 MessageListView::MessageListView(MessageCenterView
* message_center_view
,
26 : message_center_view_(message_center_view
),
29 has_deferred_task_(false),
30 clear_all_started_(false),
32 weak_ptr_factory_(this) {
33 views::BoxLayout
* layout
=
34 new views::BoxLayout(views::BoxLayout::kVertical
, 0, 0, 1);
35 layout
->SetDefaultFlex(1);
36 SetLayoutManager(layout
);
38 // Set the margin to 0 for the layout. BoxLayout assumes the same margin
39 // for top and bottom, but the bottom margin here should be smaller
40 // because of the shadow of message view. Use an empty border instead
41 // to provide this margin.
42 gfx::Insets shadow_insets
= MessageView::GetShadowInsets();
44 views::Background::CreateSolidBackground(kMessageCenterBackgroundColor
));
45 SetBorder(views::Border::CreateEmptyBorder(
46 top_down
? 0 : kMarginBetweenItems
- shadow_insets
.top(), /* top */
47 kMarginBetweenItems
- shadow_insets
.left(), /* left */
48 top_down
? kMarginBetweenItems
- shadow_insets
.bottom() : 0, /* bottom */
49 kMarginBetweenItems
- shadow_insets
.right() /* right */));
52 MessageListView::~MessageListView() {
54 animator_
->RemoveObserver(this);
57 void MessageListView::Layout() {
58 if (animator_
.get() && animator_
->IsAnimating())
61 gfx::Rect child_area
= GetContentsBounds();
62 int top
= child_area
.y();
64 kMarginBetweenItems
- MessageView::GetShadowInsets().bottom();
66 for (int i
= 0; i
< child_count(); ++i
) {
67 views::View
* child
= child_at(i
);
68 if (!child
->visible())
70 int height
= child
->GetHeightForWidth(child_area
.width());
71 child
->SetBounds(child_area
.x(), top
, child_area
.width(), height
);
72 top
+= height
+ between_items
;
76 void MessageListView::AddNotificationAt(MessageView
* view
, int index
) {
77 // |index| refers to a position in a subset of valid children. |real_index|
78 // in a list includes the invalid children, so we compute the real index by
79 // walking the list until |index| number of valid children are encountered,
80 // or to the end of the list.
82 while (real_index
< child_count()) {
83 if (IsValidChild(child_at(real_index
))) {
91 AddChildViewAt(view
, real_index
);
92 if (GetContentsBounds().IsEmpty())
95 adding_views_
.insert(view
);
99 void MessageListView::RemoveNotification(MessageView
* view
) {
100 DCHECK_EQ(view
->parent(), this);
101 if (GetContentsBounds().IsEmpty()) {
105 deleting_views_
.insert(view
);
108 animator_
->StopAnimatingView(view
);
111 DoUpdateIfPossible();
115 void MessageListView::UpdateNotification(MessageView
* view
,
116 const Notification
& notification
) {
117 int index
= GetIndexOf(view
);
118 DCHECK_LE(0, index
); // GetIndexOf is negative if not a child.
121 animator_
->StopAnimatingView(view
);
122 if (deleting_views_
.find(view
) != deleting_views_
.end())
123 deleting_views_
.erase(view
);
124 if (deleted_when_done_
.find(view
) != deleted_when_done_
.end())
125 deleted_when_done_
.erase(view
);
126 view
->UpdateWithNotification(notification
);
127 DoUpdateIfPossible();
130 gfx::Size
MessageListView::GetPreferredSize() const {
132 for (int i
= 0; i
< child_count(); i
++) {
133 const views::View
* child
= child_at(i
);
134 if (IsValidChild(child
))
135 width
= std::max(width
, child
->GetPreferredSize().width());
138 return gfx::Size(width
+ GetInsets().width(),
139 GetHeightForWidth(width
+ GetInsets().width()));
142 int MessageListView::GetHeightForWidth(int width
) const {
143 if (fixed_height_
> 0)
144 return fixed_height_
;
146 width
-= GetInsets().width();
149 for (int i
= 0; i
< child_count(); ++i
) {
150 const views::View
* child
= child_at(i
);
151 if (!IsValidChild(child
))
153 height
+= child
->GetHeightForWidth(width
) + padding
;
154 padding
= kMarginBetweenItems
- MessageView::GetShadowInsets().bottom();
157 return height
+ GetInsets().height();
160 void MessageListView::PaintChildren(const ui::PaintContext
& context
) {
161 // Paint in the inversed order. Otherwise upper notification may be
162 // hidden by the lower one.
163 for (int i
= child_count() - 1; i
>= 0; --i
) {
164 if (!child_at(i
)->layer())
165 child_at(i
)->Paint(context
);
169 void MessageListView::ReorderChildLayers(ui::Layer
* parent_layer
) {
170 // Reorder children to stack the last child layer at the top. Otherwise
171 // upper notification may be hidden by the lower one.
172 for (int i
= 0; i
< child_count(); ++i
) {
173 if (child_at(i
)->layer())
174 parent_layer
->StackAtBottom(child_at(i
)->layer());
178 void MessageListView::SetRepositionTarget(const gfx::Rect
& target
) {
179 reposition_top_
= target
.y();
180 fixed_height_
= GetHeightForWidth(width());
183 void MessageListView::ResetRepositionSession() {
184 // Don't call DoUpdateIfPossible(), but let Layout() do the task without
185 // animation. Reset will cause the change of the bubble size itself, and
186 // animation from the old location will look weird.
187 if (reposition_top_
>= 0 && animator_
.get()) {
188 has_deferred_task_
= false;
189 // cancel cause OnBoundsAnimatorDone which deletes |deleted_when_done_|.
191 STLDeleteContainerPointers(deleting_views_
.begin(), deleting_views_
.end());
192 deleting_views_
.clear();
193 adding_views_
.clear();
197 reposition_top_
= -1;
201 void MessageListView::ClearAllNotifications(
202 const gfx::Rect
& visible_scroll_rect
) {
203 for (int i
= 0; i
< child_count(); ++i
) {
204 views::View
* child
= child_at(i
);
205 if (!child
->visible())
207 if (gfx::IntersectRects(child
->bounds(), visible_scroll_rect
).IsEmpty())
209 clearing_all_views_
.push_back(child
);
211 DoUpdateIfPossible();
214 void MessageListView::OnBoundsAnimatorProgressed(
215 views::BoundsAnimator
* animator
) {
216 DCHECK_EQ(animator_
.get(), animator
);
217 for (std::set
<views::View
*>::iterator iter
= deleted_when_done_
.begin();
218 iter
!= deleted_when_done_
.end(); ++iter
) {
219 const gfx::SlideAnimation
* animation
= animator
->GetAnimationForView(*iter
);
221 (*iter
)->layer()->SetOpacity(animation
->CurrentValueBetween(1.0, 0.0));
225 void MessageListView::OnBoundsAnimatorDone(views::BoundsAnimator
* animator
) {
226 STLDeleteContainerPointers(deleted_when_done_
.begin(),
227 deleted_when_done_
.end());
228 deleted_when_done_
.clear();
230 if (clear_all_started_
) {
231 clear_all_started_
= false;
232 message_center_view()->OnAllNotificationsCleared();
235 if (has_deferred_task_
) {
236 has_deferred_task_
= false;
237 DoUpdateIfPossible();
241 GetWidget()->SynthesizeMouseMoveEvent();
244 bool MessageListView::IsValidChild(const views::View
* child
) const {
245 return child
->visible() &&
246 deleting_views_
.find(const_cast<views::View
*>(child
)) ==
247 deleting_views_
.end() &&
248 deleted_when_done_
.find(const_cast<views::View
*>(child
)) ==
249 deleted_when_done_
.end();
252 void MessageListView::DoUpdateIfPossible() {
253 gfx::Rect child_area
= GetContentsBounds();
254 if (child_area
.IsEmpty())
257 if (animator_
.get() && animator_
->IsAnimating()) {
258 has_deferred_task_
= true;
262 if (!animator_
.get()) {
263 animator_
.reset(new views::BoundsAnimator(this));
264 animator_
->AddObserver(this);
267 if (!clearing_all_views_
.empty()) {
268 AnimateClearingOneNotification();
273 base::CommandLine::ForCurrentProcess()->HasSwitch(
274 switches::kEnableMessageCenterAlwaysScrollUpUponNotificationRemoval
))
275 AnimateNotificationsBelowTarget();
277 AnimateNotificationsAboveTarget();
279 adding_views_
.clear();
280 deleting_views_
.clear();
283 void MessageListView::AnimateNotificationsBelowTarget() {
285 for (int i
= 0; i
< child_count(); ++i
) {
286 views::View
* child
= child_at(i
);
287 if (!IsValidChild(child
)) {
288 AnimateChild(child
, child
->y(), child
->height());
289 } else if (reposition_top_
< 0 || child
->y() > reposition_top_
) {
290 // Find first notification below target (or all notifications if no
296 if (last_index
> 0) {
298 kMarginBetweenItems
- MessageView::GetShadowInsets().bottom();
299 int top
= (reposition_top_
> 0) ? reposition_top_
: GetInsets().top();
301 for (int i
= last_index
; i
< child_count(); ++i
) {
302 // Animate notifications below target upwards.
303 views::View
* child
= child_at(i
);
304 if (AnimateChild(child
, top
, child
->height()))
305 top
+= child
->height() + between_items
;
310 void MessageListView::AnimateNotificationsAboveTarget() {
312 for (int i
= child_count() - 1; i
>= 0; --i
) {
313 views::View
* child
= child_at(i
);
314 if (!IsValidChild(child
)) {
315 AnimateChild(child
, child
->y(), child
->height());
316 } else if (reposition_top_
< 0 || child
->y() < reposition_top_
) {
317 // Find first notification above target (or all notifications if no
323 if (last_index
>= 0) {
325 kMarginBetweenItems
- MessageView::GetShadowInsets().bottom();
326 int bottom
= (reposition_top_
> 0)
327 ? reposition_top_
+ child_at(last_index
)->height()
328 : GetHeightForWidth(width()) - GetInsets().bottom();
329 for (int i
= last_index
; i
>= 0; --i
) {
330 // Animate notifications above target downwards.
331 views::View
* child
= child_at(i
);
332 if (AnimateChild(child
, bottom
- child
->height(), child
->height()))
333 bottom
-= child
->height() + between_items
;
338 bool MessageListView::AnimateChild(views::View
* child
, int top
, int height
) {
339 gfx::Rect child_area
= GetContentsBounds();
340 if (adding_views_
.find(child
) != adding_views_
.end()) {
341 child
->SetBounds(child_area
.right(), top
, child_area
.width(), height
);
342 animator_
->AnimateViewTo(
343 child
, gfx::Rect(child_area
.x(), top
, child_area
.width(), height
));
344 } else if (deleting_views_
.find(child
) != deleting_views_
.end()) {
345 DCHECK(child
->layer());
346 // No moves, but animate to fade-out.
347 animator_
->AnimateViewTo(child
, child
->bounds());
348 deleted_when_done_
.insert(child
);
351 gfx::Rect
target(child_area
.x(), top
, child_area
.width(), height
);
352 if (child
->bounds().origin() != target
.origin())
353 animator_
->AnimateViewTo(child
, target
);
355 child
->SetBoundsRect(target
);
360 void MessageListView::AnimateClearingOneNotification() {
361 DCHECK(!clearing_all_views_
.empty());
363 clear_all_started_
= true;
365 views::View
* child
= clearing_all_views_
.front();
366 clearing_all_views_
.pop_front();
368 // Slide from left to right.
369 gfx::Rect new_bounds
= child
->bounds();
370 new_bounds
.set_x(new_bounds
.right() + kMarginBetweenItems
);
371 animator_
->AnimateViewTo(child
, new_bounds
);
373 // Schedule to start sliding out next notification after a short delay.
374 if (!clearing_all_views_
.empty()) {
375 base::MessageLoop::current()->PostDelayedTask(
376 FROM_HERE
, base::Bind(&MessageListView::AnimateClearingOneNotification
,
377 weak_ptr_factory_
.GetWeakPtr()),
378 base::TimeDelta::FromMilliseconds(
379 kAnimateClearingNextNotificationDelayMS
));
383 } // namespace message_center