1 // Copyright (c) 2013 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 "ui/message_center/views/message_popup_collection.h"
6
7 #include "base/bind.h"
8 #include "base/stl_util.h"
9 #include "base/threading/thread_task_runner_handle.h"
10 #include "ui/gfx/animation/linear_animation.h"
11 #include "ui/gfx/animation/tween.h"
12 #include "ui/message_center/message_center.h"
13 #include "ui/message_center/public/cpp/message_center_constants.h"
14 #include "ui/message_center/views/message_popup_view.h"
15
16 namespace message_center {
17
18 namespace {
19
20 // Animation duration for FADE_IN and FADE_OUT.
21 constexpr base::TimeDelta kFadeInFadeOutDuration =
22 base::TimeDelta::FromMilliseconds(200);
23
24 // Animation duration for MOVE_DOWN.
25 constexpr base::TimeDelta kMoveDownDuration =
26 base::TimeDelta::FromMilliseconds(120);
27
28 } // namespace
29
MessagePopupCollection()30 MessagePopupCollection::MessagePopupCollection()
31 : animation_(std::make_unique<gfx::LinearAnimation>(this)),
32 weak_ptr_factory_(this) {
33 MessageCenter::Get()->AddObserver(this);
34 }
35
~MessagePopupCollection()36 MessagePopupCollection::~MessagePopupCollection() {
37 for (const auto& item : popup_items_)
38 item.popup->Close();
39 MessageCenter::Get()->RemoveObserver(this);
40 }
41
Update()42 void MessagePopupCollection::Update() {
43 if (is_updating_)
44 return;
45 base::AutoReset<bool> reset(&is_updating_, true);
46
47 RemoveClosedPopupItems();
48
49 if (MessageCenter::Get()->IsMessageCenterVisible()) {
50 CloseAllPopupsNow();
51 return;
52 }
53
54 if (animation_->is_animating()) {
55 UpdateByAnimation();
56 return;
57 }
58
59 if (state_ != State::IDLE)
60 TransitionFromAnimation();
61
62 if (state_ == State::IDLE)
63 TransitionToAnimation();
64
65 UpdatePopupTimers();
66
67 if (state_ != State::IDLE) {
68 // If not in IDLE state, start animation.
69 animation_->SetDuration(state_ == State::MOVE_DOWN ||
70 state_ == State::MOVE_UP_FOR_INVERSE
71 ? kMoveDownDuration
72 : kFadeInFadeOutDuration);
73 animation_->Start();
74 UpdateByAnimation();
75 }
76
77 DCHECK(state_ == State::IDLE || animation_->is_animating());
78 }
79
ResetBounds()80 void MessagePopupCollection::ResetBounds() {
81 if (is_updating_)
82 return;
83 {
84 base::AutoReset<bool> reset(&is_updating_, true);
85
86 RemoveClosedPopupItems();
87 ResetHotMode();
88 state_ = State::IDLE;
89 animation_->End();
90
91 CalculateBounds();
92
93 // Remove popups that are no longer in work area.
94 ClosePopupsOutsideWorkArea();
95
96 // Reset bounds and opacity of popups.
97 for (auto& item : popup_items_) {
98 item.popup->SetPopupBounds(item.bounds);
99 item.popup->SetOpacity(1.0);
100 }
101 }
102
103 // Restart animation for FADE_OUT.
104 Update();
105 }
106
NotifyPopupResized()107 void MessagePopupCollection::NotifyPopupResized() {
108 resize_requested_ = true;
109 Update();
110 }
111
NotifyPopupClosed(MessagePopupView * popup)112 void MessagePopupCollection::NotifyPopupClosed(MessagePopupView* popup) {
113 for (auto& item : popup_items_) {
114 if (item.popup == popup)
115 item.popup = nullptr;
116 }
117 }
118
OnNotificationAdded(const std::string & notification_id)119 void MessagePopupCollection::OnNotificationAdded(
120 const std::string& notification_id) {
121 // Should not call MessagePopupCollection::Update here. Because notification
122 // may be removed before animation which is triggered by the previous
123 // operation on MessagePopupCollection ends. As result, when a new
124 // notification with the same ID is created, calling
125 // MessagePopupCollection::Update will not update the popup's content. Then
126 // the new notification popup fails to show. (see https://crbug.com/921402)
127 OnNotificationUpdated(notification_id);
128 }
129
OnNotificationRemoved(const std::string & notification_id,bool by_user)130 void MessagePopupCollection::OnNotificationRemoved(
131 const std::string& notification_id,
132 bool by_user) {
133 Update();
134 }
135
OnNotificationUpdated(const std::string & notification_id)136 void MessagePopupCollection::OnNotificationUpdated(
137 const std::string& notification_id) {
138 if (is_updating_)
139 return;
140
141 // Find Notification object with |notification_id|.
142 const auto& notifications = MessageCenter::Get()->GetPopupNotifications();
143 auto it = notifications.begin();
144 while (it != notifications.end()) {
145 if ((*it)->id() == notification_id)
146 break;
147 ++it;
148 }
149
150 if (it == notifications.end()) {
151 // If not found, probably |notification_id| is removed from popups by
152 // timeout.
153 Update();
154 return;
155 }
156
157 {
158 base::AutoReset<bool> reset(&is_updating_, true);
159
160 RemoveClosedPopupItems();
161
162 // Update contents of the notification.
163 for (const auto& item : popup_items_) {
164 if (item.id == notification_id) {
165 item.popup->UpdateContents(**it);
166 break;
167 }
168 }
169 }
170
171 Update();
172 }
173
OnCenterVisibilityChanged(Visibility visibility)174 void MessagePopupCollection::OnCenterVisibilityChanged(Visibility visibility) {
175 Update();
176 }
177
OnBlockingStateChanged(NotificationBlocker * blocker)178 void MessagePopupCollection::OnBlockingStateChanged(
179 NotificationBlocker* blocker) {
180 Update();
181 }
182
AnimationEnded(const gfx::Animation * animation)183 void MessagePopupCollection::AnimationEnded(const gfx::Animation* animation) {
184 Update();
185 }
186
AnimationProgressed(const gfx::Animation * animation)187 void MessagePopupCollection::AnimationProgressed(
188 const gfx::Animation* animation) {
189 Update();
190 }
191
AnimationCanceled(const gfx::Animation * animation)192 void MessagePopupCollection::AnimationCanceled(
193 const gfx::Animation* animation) {
194 Update();
195 }
196
GetPopupViewForNotificationID(const std::string & notification_id)197 MessagePopupView* MessagePopupCollection::GetPopupViewForNotificationID(
198 const std::string& notification_id) {
199 for (const auto& item : popup_items_) {
200 if (item.id == notification_id)
201 return item.popup;
202 }
203 return nullptr;
204 }
205
CreatePopup(const Notification & notification)206 MessagePopupView* MessagePopupCollection::CreatePopup(
207 const Notification& notification) {
208 return new MessagePopupView(notification, this);
209 }
210
RestartPopupTimers()211 void MessagePopupCollection::RestartPopupTimers() {
212 MessageCenter::Get()->RestartPopupTimers();
213 }
214
PausePopupTimers()215 void MessagePopupCollection::PausePopupTimers() {
216 MessageCenter::Get()->PausePopupTimers();
217 }
218
TransitionFromAnimation()219 void MessagePopupCollection::TransitionFromAnimation() {
220 DCHECK_NE(state_, State::IDLE);
221 DCHECK(!animation_->is_animating());
222
223 // The animation of type |state_| is now finished.
224 UpdateByAnimation();
225
226 // If FADE_OUT animation is finished, remove the animated popup.
227 if (state_ == State::FADE_OUT)
228 CloseAnimatingPopups();
229
230 if (state_ == State::FADE_IN || state_ == State::MOVE_DOWN ||
231 (state_ == State::FADE_OUT && popup_items_.empty())) {
232 // If the animation is finished, transition to IDLE.
233 state_ = State::IDLE;
234 } else if (state_ == State::FADE_OUT && !popup_items_.empty()) {
235 if ((HasAddedPopup() && CollapseAllPopups()) || !inverse_) {
236 // If FADE_OUT animation is finished and we still have remaining popups,
237 // we have to MOVE_DOWN them.
238 // If we're going to add a new popup after this MOVE_DOWN, do the collapse
239 // animation at the same time. Otherwise it will take another MOVE_DOWN.
240 state_ = State::MOVE_DOWN;
241 MoveDownPopups();
242 } else {
243 // If there's no collapsable popups and |inverse_| is on, there's nothing
244 // to do after FADE_OUT.
245 state_ = State::IDLE;
246 }
247 } else if (state_ == State::MOVE_UP_FOR_INVERSE) {
248 for (auto& item : popup_items_)
249 item.is_animating = item.will_fade_in;
250 state_ = State::FADE_IN;
251 }
252 }
253
TransitionToAnimation()254 void MessagePopupCollection::TransitionToAnimation() {
255 DCHECK_EQ(state_, State::IDLE);
256 DCHECK(!animation_->is_animating());
257
258 if (HasRemovedPopup()) {
259 MarkRemovedPopup();
260
261 // Start hot mode to allow a user to continually close many notifications.
262 StartHotMode();
263
264 if (CloseTransparentPopups()) {
265 // If the popup is already transparent, skip FADE_OUT.
266 state_ = State::MOVE_DOWN;
267 MoveDownPopups();
268 } else {
269 state_ = State::FADE_OUT;
270 }
271 return;
272 }
273
274 if (HasAddedPopup()) {
275 if (CollapseAllPopups()) {
276 // If we had existing popups that weren't collapsed, first show collapsing
277 // animation.
278 state_ = State::MOVE_DOWN;
279 MoveDownPopups();
280 return;
281 } else if (AddPopup()) {
282 // A popup is actually added.
283 if (inverse_ && popup_items_.size() > 1) {
284 // If |inverse_| is on and there are existing notifications that have to
285 // be moved up (existing ones + new one, so > 1), transition to
286 // MOVE_UP_FOR_INVERSE.
287 state_ = State::MOVE_UP_FOR_INVERSE;
288 } else {
289 // Show FADE_IN animation.
290 state_ = State::FADE_IN;
291 }
292 return;
293 }
294 }
295
296 if (resize_requested_) {
297 // Resize is requested e.g. a user manually expanded notification.
298 resize_requested_ = false;
299 state_ = State::MOVE_DOWN;
300 MoveDownPopups();
301
302 // This function may be called by a child MessageView when a notification is
303 // expanded by the user. Deleting the pop-up should be delayed so we are
304 // out of the child view's call stack. See crbug.com/957033.
305 base::ThreadTaskRunnerHandle::Get()->PostTask(
306 FROM_HERE,
307 base::BindOnce(&MessagePopupCollection::ClosePopupsOutsideWorkArea,
308 weak_ptr_factory_.GetWeakPtr()));
309 return;
310 }
311
312 if (!IsAnyPopupHovered() && is_hot_) {
313 // Reset hot mode and animate to the normal positions.
314 state_ = State::MOVE_DOWN;
315 ResetHotMode();
316 MoveDownPopups();
317 }
318 }
319
UpdatePopupTimers()320 void MessagePopupCollection::UpdatePopupTimers() {
321 if (state_ == State::IDLE) {
322 if (IsAnyPopupHovered() || IsAnyPopupActive()) {
323 // If any popup is hovered or activated, pause popup timer.
324 PausePopupTimers();
325 } else {
326 // If in IDLE state, restart popup timer.
327 RestartPopupTimers();
328 }
329 } else {
330 // If not in IDLE state, pause popup timer.
331 PausePopupTimers();
332 }
333 }
334
CalculateBounds()335 void MessagePopupCollection::CalculateBounds() {
336 int base = GetBaseline();
337 for (size_t i = 0; i < popup_items_.size(); ++i) {
338 gfx::Size preferred_size(
339 kNotificationWidth,
340 GetPopupItem(i)->popup->GetHeightForWidth(kNotificationWidth));
341
342 // Align the top of i-th popup to |hot_top_|.
343 if (is_hot_ && hot_index_ == i) {
344 base = hot_top_;
345 if (!IsTopDown())
346 base += preferred_size.height();
347 }
348
349 int origin_x = GetToastOriginX(gfx::Rect(preferred_size));
350
351 int origin_y = base;
352 if (!IsTopDown())
353 origin_y -= preferred_size.height();
354
355 GetPopupItem(i)->start_bounds = GetPopupItem(i)->bounds;
356 GetPopupItem(i)->bounds =
357 gfx::Rect(gfx::Point(origin_x, origin_y), preferred_size);
358
359 const int delta = preferred_size.height() + kMarginBetweenPopups;
360 if (IsTopDown())
361 base += delta;
362 else
363 base -= delta;
364 }
365 }
366
UpdateByAnimation()367 void MessagePopupCollection::UpdateByAnimation() {
368 DCHECK_NE(state_, State::IDLE);
369
370 for (auto& item : popup_items_) {
371 if (!item.is_animating)
372 continue;
373
374 double value = gfx::Tween::CalculateValue(
375 state_ == State::FADE_OUT ? gfx::Tween::EASE_IN : gfx::Tween::EASE_OUT,
376 animation_->GetCurrentValue());
377
378 if (state_ == State::FADE_IN)
379 item.popup->SetOpacity(gfx::Tween::FloatValueBetween(value, 0.0f, 1.0f));
380 else if (state_ == State::FADE_OUT)
381 item.popup->SetOpacity(gfx::Tween::FloatValueBetween(value, 1.0f, 0.0f));
382
383 if (state_ == State::FADE_IN || state_ == State::MOVE_DOWN ||
384 state_ == State::MOVE_UP_FOR_INVERSE) {
385 item.popup->SetPopupBounds(
386 gfx::Tween::RectValueBetween(value, item.start_bounds, item.bounds));
387 }
388 }
389 }
390
AddPopup()391 bool MessagePopupCollection::AddPopup() {
392 std::set<std::string> existing_ids;
393 for (const auto& item : popup_items_)
394 existing_ids.insert(item.id);
395
396 auto notifications = MessageCenter::Get()->GetPopupNotifications();
397 Notification* new_notification = nullptr;
398 // Reverse iterating because notifications are in reverse chronological order.
399 for (auto it = notifications.rbegin(); it != notifications.rend(); ++it) {
400 // Disables popup of custom notification on non-primary displays, since
401 // currently custom notification supports only on one display at the same
402 // time.
403 // TODO(yoshiki): Support custom popup notification on multiple display
404 // (https://crbug.com/715370).
405 if (!IsPrimaryDisplayForNotification() &&
406 (*it)->type() == NOTIFICATION_TYPE_CUSTOM) {
407 continue;
408 }
409
410 if (!existing_ids.count((*it)->id())) {
411 new_notification = *it;
412 break;
413 }
414 }
415
416 if (!new_notification)
417 return false;
418
419 // Reset animation flags of existing popups.
420 for (auto& item : popup_items_) {
421 item.is_animating = false;
422 item.will_fade_in = false;
423 }
424
425 {
426 PopupItem item;
427 item.id = new_notification->id();
428 item.is_animating = true;
429 item.popup = CreatePopup(*new_notification);
430
431 if (IsNextEdgeOutsideWorkArea(item)) {
432 item.popup->Close();
433 return false;
434 }
435
436 item.popup->Show();
437 popup_items_.push_back(item);
438 NotifyPopupAdded(item.popup);
439 }
440
441 // There are existing notifications that have to be moved up (existing ones +
442 // new one, so > 1).
443 if (inverse_ && popup_items_.size() > 1) {
444 for (auto& item : popup_items_) {
445 item.will_fade_in = item.is_animating;
446 item.is_animating = !item.is_animating;
447 }
448 }
449
450 MessageCenter::Get()->DisplayedNotification(new_notification->id(),
451 DISPLAY_SOURCE_POPUP);
452
453 CalculateBounds();
454
455 auto& item = popup_items_.back();
456 item.start_bounds = item.bounds;
457 item.start_bounds +=
458 gfx::Vector2d((IsFromLeft() ? -1 : 1) * item.bounds.width(), 0);
459 return true;
460 }
461
MarkRemovedPopup()462 void MessagePopupCollection::MarkRemovedPopup() {
463 std::set<std::string> existing_ids;
464 for (Notification* notification :
465 MessageCenter::Get()->GetPopupNotifications()) {
466 existing_ids.insert(notification->id());
467 }
468
469 for (auto& item : popup_items_)
470 item.is_animating = !existing_ids.count(item.id);
471 }
472
MoveDownPopups()473 void MessagePopupCollection::MoveDownPopups() {
474 CalculateBounds();
475 for (auto& item : popup_items_)
476 item.is_animating = true;
477 }
478
GetNextEdge(const PopupItem & item) const479 int MessagePopupCollection::GetNextEdge(const PopupItem& item) const {
480 const int delta =
481 item.popup->GetHeightForWidth(kNotificationWidth) + kMarginBetweenPopups;
482
483 int base = 0;
484 if (popup_items_.empty()) {
485 base = GetBaseline();
486 } else if (inverse_) {
487 base = IsTopDown() ? popup_items_.front().bounds.bottom()
488 : popup_items_.front().bounds.y();
489 } else {
490 base = IsTopDown() ? popup_items_.back().bounds.bottom()
491 : popup_items_.back().bounds.y();
492 }
493
494 return IsTopDown() ? base + delta : base - delta;
495 }
496
IsNextEdgeOutsideWorkArea(const PopupItem & item) const497 bool MessagePopupCollection::IsNextEdgeOutsideWorkArea(
498 const PopupItem& item) const {
499 const int next_edge = GetNextEdge(item);
500 const gfx::Rect work_area = GetWorkArea();
501 return IsTopDown() ? next_edge > work_area.bottom()
502 : next_edge < work_area.y();
503 }
504
StartHotMode()505 void MessagePopupCollection::StartHotMode() {
506 for (size_t i = 0; i < popup_items_.size(); ++i) {
507 if (GetPopupItem(i)->is_animating && GetPopupItem(i)->popup->is_hovered()) {
508 is_hot_ = true;
509 hot_index_ = i;
510 hot_top_ = GetPopupItem(i)->bounds.y();
511 break;
512 }
513 }
514 }
515
ResetHotMode()516 void MessagePopupCollection::ResetHotMode() {
517 is_hot_ = false;
518 hot_index_ = 0;
519 hot_top_ = 0;
520 }
521
CloseAnimatingPopups()522 void MessagePopupCollection::CloseAnimatingPopups() {
523 for (auto& item : popup_items_) {
524 if (!item.is_animating)
525 continue;
526 item.popup->Close();
527 }
528 RemoveClosedPopupItems();
529 }
530
CloseTransparentPopups()531 bool MessagePopupCollection::CloseTransparentPopups() {
532 bool removed = false;
533 for (auto& item : popup_items_) {
534 if (item.popup->GetOpacity() > 0.0)
535 continue;
536 item.popup->Close();
537 removed = true;
538 }
539 RemoveClosedPopupItems();
540 return removed;
541 }
542
ClosePopupsOutsideWorkArea()543 void MessagePopupCollection::ClosePopupsOutsideWorkArea() {
544 const gfx::Rect work_area = GetWorkArea();
545 for (auto& item : popup_items_) {
546 if (work_area.Contains(item.bounds))
547 continue;
548 item.popup->Close();
549 }
550 RemoveClosedPopupItems();
551 }
552
RemoveClosedPopupItems()553 void MessagePopupCollection::RemoveClosedPopupItems() {
554 base::EraseIf(popup_items_, [](const auto& item) { return !item.popup; });
555 }
556
CloseAllPopupsNow()557 void MessagePopupCollection::CloseAllPopupsNow() {
558 for (auto& item : popup_items_)
559 item.is_animating = true;
560 CloseAnimatingPopups();
561
562 ResetHotMode();
563 state_ = State::IDLE;
564 animation_->End();
565 }
566
CollapseAllPopups()567 bool MessagePopupCollection::CollapseAllPopups() {
568 bool changed = false;
569 for (auto& item : popup_items_) {
570 int old_height = item.popup->GetHeightForWidth(kNotificationWidth);
571
572 item.popup->AutoCollapse();
573
574 int new_height = item.popup->GetHeightForWidth(kNotificationWidth);
575 if (old_height != new_height)
576 changed = true;
577 }
578
579 resize_requested_ = false;
580 return changed;
581 }
582
HasAddedPopup() const583 bool MessagePopupCollection::HasAddedPopup() const {
584 std::set<std::string> existing_ids;
585 for (const auto& item : popup_items_)
586 existing_ids.insert(item.id);
587
588 for (Notification* notification :
589 MessageCenter::Get()->GetPopupNotifications()) {
590 if (!existing_ids.count(notification->id()))
591 return true;
592 }
593 return false;
594 }
595
HasRemovedPopup() const596 bool MessagePopupCollection::HasRemovedPopup() const {
597 std::set<std::string> existing_ids;
598 for (Notification* notification :
599 MessageCenter::Get()->GetPopupNotifications()) {
600 existing_ids.insert(notification->id());
601 }
602
603 for (const auto& item : popup_items_) {
604 if (!existing_ids.count(item.id))
605 return true;
606 }
607 return false;
608 }
609
IsAnyPopupHovered() const610 bool MessagePopupCollection::IsAnyPopupHovered() const {
611 for (const auto& item : popup_items_) {
612 if (item.popup->is_hovered())
613 return true;
614 }
615 return false;
616 }
617
IsAnyPopupActive() const618 bool MessagePopupCollection::IsAnyPopupActive() const {
619 for (const auto& item : popup_items_) {
620 if (item.popup->is_active())
621 return true;
622 }
623 return false;
624 }
625
GetPopupItem(size_t index_from_top)626 MessagePopupCollection::PopupItem* MessagePopupCollection::GetPopupItem(
627 size_t index_from_top) {
628 DCHECK_LT(index_from_top, popup_items_.size());
629 return &popup_items_[inverse_ ? popup_items_.size() - index_from_top - 1
630 : index_from_top];
631 }
632
633 } // namespace message_center
634