1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
|
// Copyright (c) 2013 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.
#import "ui/message_center/cocoa/popup_collection.h"
#import "ui/message_center/cocoa/notification_controller.h"
#import "ui/message_center/cocoa/popup_controller.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_observer.h"
#include "ui/message_center/message_center_style.h"
const float kAnimationDuration = 0.2;
@interface MCPopupCollection (Private)
// Returns the primary screen's visible frame rectangle.
- (NSRect)screenFrame;
// Shows a popup, if there is room on-screen, for the given notification.
// Returns YES if the notification was actually displayed.
- (BOOL)addNotification:(const message_center::Notification*)notification;
// Updates the contents of the notification with the given ID.
- (void)updateNotification:(const std::string&)notificationID;
// Removes a popup from the screen and lays out new notifications that can
// now potentially fit on the screen.
- (void)removeNotification:(const std::string&)notificationID;
// Closes all the popups.
- (void)removeAllNotifications;
// Returns the index of the popup showing the notification with the given ID.
- (NSUInteger)indexOfPopupWithNotificationID:(const std::string&)notificationID;
// Repositions all popup notifications if needed.
- (void)layoutNotifications;
// Fits as many new notifications as possible on screen.
- (void)layoutNewNotifications;
// Process notifications pending to remove when no animation is being played.
- (void)processPendingRemoveNotifications;
// Process notifications pending to update when no animation is being played.
- (void)processPendingUpdateNotifications;
@end
namespace {
class PopupCollectionObserver : public message_center::MessageCenterObserver {
public:
PopupCollectionObserver(message_center::MessageCenter* message_center,
MCPopupCollection* popup_collection)
: message_center_(message_center),
popup_collection_(popup_collection) {
message_center_->AddObserver(this);
}
virtual ~PopupCollectionObserver() {
message_center_->RemoveObserver(this);
}
virtual void OnNotificationAdded(
const std::string& notification_id) OVERRIDE {
[popup_collection_ layoutNewNotifications];
}
virtual void OnNotificationRemoved(const std::string& notification_id,
bool user_id) OVERRIDE {
[popup_collection_ removeNotification:notification_id];
}
virtual void OnNotificationUpdated(
const std::string& notification_id) OVERRIDE {
[popup_collection_ updateNotification:notification_id];
}
private:
message_center::MessageCenter* message_center_; // Weak, global.
MCPopupCollection* popup_collection_; // Weak, owns this.
};
} // namespace
@implementation MCPopupCollection
- (id)initWithMessageCenter:(message_center::MessageCenter*)messageCenter {
if ((self = [super init])) {
messageCenter_ = messageCenter;
observer_.reset(new PopupCollectionObserver(messageCenter_, self));
popups_.reset([[NSMutableArray alloc] init]);
popupsBeingRemoved_.reset([[NSMutableArray alloc] init]);
popupAnimationDuration_ = kAnimationDuration;
}
return self;
}
- (void)dealloc {
[popupsBeingRemoved_ makeObjectsPerformSelector:
@selector(markPopupCollectionGone)];
[self removeAllNotifications];
[super dealloc];
}
- (BOOL)isAnimating {
return !animatingNotificationIDs_.empty();
}
- (NSTimeInterval)popupAnimationDuration {
return popupAnimationDuration_;
}
- (void)onPopupAnimationEnded:(const std::string&)notificationID {
NSUInteger index = [popupsBeingRemoved_ indexOfObjectPassingTest:
^BOOL(id popup, NSUInteger index, BOOL* stop) {
return [popup notificationID] == notificationID;
}];
if (index != NSNotFound)
[popupsBeingRemoved_ removeObjectAtIndex:index];
animatingNotificationIDs_.erase(notificationID);
if (![self isAnimating])
[self layoutNotifications];
// Give the testing code a chance to do something, i.e. quitting the test
// run loop.
if (![self isAnimating] && testingAnimationEndedCallback_)
testingAnimationEndedCallback_.get()();
}
// Testing API /////////////////////////////////////////////////////////////////
- (NSArray*)popups {
return popups_.get();
}
- (void)setScreenFrame:(NSRect)frame {
testingScreenFrame_ = frame;
}
- (void)setAnimationDuration:(NSTimeInterval)duration {
popupAnimationDuration_ = duration;
}
- (void)setAnimationEndedCallback:
(message_center::AnimationEndedCallback)callback {
testingAnimationEndedCallback_.reset(Block_copy(callback));
}
// Private /////////////////////////////////////////////////////////////////////
- (NSRect)screenFrame {
if (!NSIsEmptyRect(testingScreenFrame_))
return testingScreenFrame_;
return [[[NSScreen screens] objectAtIndex:0] visibleFrame];
}
- (BOOL)addNotification:(const message_center::Notification*)notification {
// Wait till all existing animations end.
if ([self isAnimating])
return NO;
// The popup is owned by itself. It will be released at close.
MCPopupController* popup =
[[MCPopupController alloc] initWithNotification:notification
messageCenter:messageCenter_
popupCollection:self];
NSRect screenFrame = [self screenFrame];
NSRect popupFrame = [popup bounds];
CGFloat x = NSMaxX(screenFrame) - message_center::kMarginBetweenItems -
NSWidth(popupFrame);
CGFloat y = 0;
MCPopupController* bottomPopup = [popups_ lastObject];
if (!bottomPopup) {
y = NSMaxY(screenFrame);
} else {
y = NSMinY([bottomPopup bounds]);
}
y -= message_center::kMarginBetweenItems + NSHeight(popupFrame);
if (y > NSMinY(screenFrame)) {
animatingNotificationIDs_.insert(notification->id());
NSRect bounds = [popup bounds];
bounds.origin.x = x;
bounds.origin.y = y;
[popup showWithAnimation:bounds];
[popups_ addObject:popup];
messageCenter_->DisplayedNotification(notification->id());
return YES;
}
// The popup cannot fit on screen, so it has to be released now.
[popup release];
return NO;
}
- (void)updateNotification:(const std::string&)notificationID {
// The notification may not be on screen.
if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound)
return;
// Don't bother with the update if the notification is going to be removed.
if (pendingRemoveNotificationIDs_.find(notificationID) !=
pendingRemoveNotificationIDs_.end()) {
return;
}
pendingUpdateNotificationIDs_.insert(notificationID);
[self processPendingUpdateNotifications];
}
- (void)removeNotification:(const std::string&)notificationID {
// The notification may not be on screen.
if ([self indexOfPopupWithNotificationID:notificationID] == NSNotFound)
return;
// Don't bother with the update if the notification is going to be removed.
pendingUpdateNotificationIDs_.erase(notificationID);
pendingRemoveNotificationIDs_.insert(notificationID);
[self processPendingRemoveNotifications];
}
- (void)removeAllNotifications {
// In rare cases, the popup collection would be gone while an animation is
// still playing. For exmaple, the test code could show a new notification
// and dispose the collection immediately. Close the popup without animation
// when this is the case.
if ([self isAnimating])
[popups_ makeObjectsPerformSelector:@selector(close)];
else
[popups_ makeObjectsPerformSelector:@selector(closeWithAnimation)];
[popups_ makeObjectsPerformSelector:@selector(markPopupCollectionGone)];
[popups_ removeAllObjects];
}
- (NSUInteger)indexOfPopupWithNotificationID:
(const std::string&)notificationID {
return [popups_ indexOfObjectPassingTest:
^BOOL(id popup, NSUInteger index, BOOL* stop) {
return [popup notificationID] == notificationID;
}];
}
- (void)layoutNotifications {
// Wait till all existing animations end.
if ([self isAnimating])
return;
NSRect screenFrame = [self screenFrame];
// The popup starts at top-right corner.
CGFloat maxY = NSMaxY(screenFrame);
// Iterate all notifications and reposition each if needed. If one does not
// fit on screen, close it and any other on-screen popups that come after it.
NSUInteger removeAt = NSNotFound;
for (NSUInteger i = 0; i < [popups_ count]; ++i) {
MCPopupController* popup = [popups_ objectAtIndex:i];
NSRect oldFrame = [popup bounds];
NSRect frame = oldFrame;
frame.origin.y = maxY - message_center::kMarginBetweenItems -
NSHeight(frame);
// If this popup does not fit on screen, stop repositioning and close this
// and subsequent popups.
if (NSMinY(frame) < NSMinY(screenFrame)) {
removeAt = i;
break;
}
if (!NSEqualRects(frame, oldFrame)) {
[popup setBounds:frame];
animatingNotificationIDs_.insert([popup notificationID]);
}
// Set the new maximum Y to be the bottom of this notification.
maxY = NSMinY(frame);
}
if (removeAt != NSNotFound) {
// Remove any popups that are on screen but no longer fit.
while ([popups_ count] >= removeAt && [popups_ count]) {
[[popups_ lastObject] close];
[popups_ removeLastObject];
}
} else {
[self layoutNewNotifications];
}
[self processPendingRemoveNotifications];
[self processPendingUpdateNotifications];
}
- (void)layoutNewNotifications {
// Wait till all existing animations end.
if ([self isAnimating])
return;
// Display any new popups that can now fit on screen, starting from the
// oldest notification that has not been shown up.
const auto& allPopups = messageCenter_->GetPopupNotifications();
for (auto it = allPopups.rbegin(); it != allPopups.rend(); ++it) {
if ([self indexOfPopupWithNotificationID:(*it)->id()] == NSNotFound) {
// If there's no room left on screen to display notifications, stop
// trying.
if (![self addNotification:*it])
break;
}
}
}
- (void)processPendingRemoveNotifications {
// Wait till all existing animations end.
if ([self isAnimating])
return;
for (const auto& notificationID : pendingRemoveNotificationIDs_) {
NSUInteger index = [self indexOfPopupWithNotificationID:notificationID];
if (index != NSNotFound) {
[[popups_ objectAtIndex:index] closeWithAnimation];
animatingNotificationIDs_.insert(notificationID);
// Still need to track popup object and only remove it after the animation
// ends. We need to notify these objects that the collection is gone
// in the collection destructor.
[popupsBeingRemoved_ addObject:[popups_ objectAtIndex:index]];
[popups_ removeObjectAtIndex:index];
}
}
pendingRemoveNotificationIDs_.clear();
}
- (void)processPendingUpdateNotifications {
// Wait till all existing animations end.
if ([self isAnimating])
return;
if (pendingUpdateNotificationIDs_.empty())
return;
// Go through all model objects in the message center. If there is a replaced
// notification, the controller's current model object may be stale.
const auto& modelPopups = messageCenter_->GetPopupNotifications();
for (auto iter = modelPopups.begin(); iter != modelPopups.end(); ++iter) {
const std::string& notificationID = (*iter)->id();
// Does the notification need to be updated?
std::set<std::string>::iterator pendingUpdateIter =
pendingUpdateNotificationIDs_.find(notificationID);
if (pendingUpdateIter == pendingUpdateNotificationIDs_.end())
continue;
pendingUpdateNotificationIDs_.erase(pendingUpdateIter);
// Is the notification still on screen?
NSUInteger index = [self indexOfPopupWithNotificationID:notificationID];
if (index == NSNotFound)
continue;
MCPopupController* popup = [popups_ objectAtIndex:index];
CGFloat oldHeight =
NSHeight([[[popup notificationController] view] frame]);
CGFloat newHeight = NSHeight(
[[popup notificationController] updateNotification:*iter]);
// The notification has changed height. This requires updating the popup
// window.
if (oldHeight != newHeight) {
NSRect popupFrame = [popup bounds];
popupFrame.origin.y -= newHeight - oldHeight;
popupFrame.size.height += newHeight - oldHeight;
[popup setBounds:popupFrame];
animatingNotificationIDs_.insert([popup notificationID]);
}
}
// Notification update could be received when a notification is excluded from
// the popup notification list but still remains in the full notification
// list, as in clicking the popup. In that case, the popup should be closed.
for (auto iter = pendingUpdateNotificationIDs_.begin();
iter != pendingUpdateNotificationIDs_.end(); ++iter) {
pendingRemoveNotificationIDs_.insert(*iter);
}
pendingUpdateNotificationIDs_.clear();
// Start re-layout of all notifications, so that it readjusts the Y origin of
// all updated popups and any popups that come below them.
[self layoutNotifications];
}
@end
|