OLD | NEW |
| (Empty) |
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 #import "ui/message_center/cocoa/tray_view_controller.h" | |
6 | |
7 #include <cmath> | |
8 | |
9 #include "base/mac/scoped_nsautorelease_pool.h" | |
10 #include "base/time/time.h" | |
11 #include "skia/ext/skia_utils_mac.h" | |
12 #import "ui/base/cocoa/hover_image_button.h" | |
13 #include "ui/base/l10n/l10n_util_mac.h" | |
14 #include "ui/base/resource/resource_bundle.h" | |
15 #import "ui/message_center/cocoa/notification_controller.h" | |
16 #import "ui/message_center/cocoa/opaque_views.h" | |
17 #import "ui/message_center/cocoa/settings_controller.h" | |
18 #include "ui/message_center/message_center.h" | |
19 #include "ui/message_center/message_center_style.h" | |
20 #include "ui/message_center/notifier_settings.h" | |
21 #include "ui/resources/grit/ui_resources.h" | |
22 #include "ui/strings/grit/ui_strings.h" | |
23 | |
24 const int kBackButtonSize = 16; | |
25 | |
26 // NSClipView subclass. | |
27 @interface MCClipView : NSClipView { | |
28 // If this is set, the visible document area will remain intact no matter how | |
29 // the user scrolls or drags the thumb. | |
30 BOOL frozen_; | |
31 } | |
32 @end | |
33 | |
34 @implementation MCClipView | |
35 - (void)setFrozen:(BOOL)frozen { | |
36 frozen_ = frozen; | |
37 } | |
38 | |
39 - (NSPoint)constrainScrollPoint:(NSPoint)proposedNewOrigin { | |
40 return frozen_ ? [self documentVisibleRect].origin : | |
41 [super constrainScrollPoint:proposedNewOrigin]; | |
42 } | |
43 @end | |
44 | |
45 @interface MCTrayViewController (Private) | |
46 // Creates all the views for the control area of the tray. | |
47 - (void)layoutControlArea; | |
48 | |
49 // Update both tray view and window by resizing it to fit its content. | |
50 - (void)updateTrayViewAndWindow; | |
51 | |
52 // Remove notifications dismissed by the user. It is done in the following | |
53 // 3 steps. | |
54 - (void)closeNotificationsByUser; | |
55 | |
56 // Step 1: hide all notifications pending removal with fade-out animation. | |
57 - (void)hideNotificationsPendingRemoval; | |
58 | |
59 // Step 2: move up all remaining notifications to take over the available space | |
60 // due to hiding notifications. The scroll view and the window remain unchanged. | |
61 - (void)moveUpRemainingNotifications; | |
62 | |
63 // Step 3: finalize the tray view and window to get rid of the empty space. | |
64 - (void)finalizeTrayViewAndWindow; | |
65 | |
66 // Clear a notification by sliding it out from left to right. This occurs when | |
67 // "Clear All" is clicked. | |
68 - (void)clearOneNotification; | |
69 | |
70 // When all visible notifications slide out, re-enable controls and remove | |
71 // notifications from the message center. | |
72 - (void)finalizeClearAll; | |
73 | |
74 // Sets the images of the quiet mode button based on the message center state. | |
75 - (void)updateQuietModeButtonImage; | |
76 @end | |
77 | |
78 namespace { | |
79 | |
80 // The duration of fade-out and bounds animation. | |
81 const NSTimeInterval kAnimationDuration = 0.2; | |
82 | |
83 // The delay to start animating clearing next notification since current | |
84 // animation starts. | |
85 const NSTimeInterval kAnimateClearingNextNotificationDelay = 0.04; | |
86 | |
87 // The height of the bar at the top of the tray that contains buttons. | |
88 const CGFloat kControlAreaHeight = 50; | |
89 | |
90 // Amount of spacing between control buttons. There is kMarginBetweenItems | |
91 // between a button and the edge of the tray, though. | |
92 const CGFloat kButtonXMargin = 20; | |
93 | |
94 // Amount of padding to leave between the bottom of the screen and the bottom | |
95 // of the message center tray. | |
96 const CGFloat kTrayBottomMargin = 75; | |
97 | |
98 } // namespace | |
99 | |
100 @implementation MCTrayViewController | |
101 | |
102 - (id)initWithMessageCenter:(message_center::MessageCenter*)messageCenter { | |
103 if ((self = [super initWithNibName:nil bundle:nil])) { | |
104 messageCenter_ = messageCenter; | |
105 animationDuration_ = kAnimationDuration; | |
106 animateClearingNextNotificationDelay_ = | |
107 kAnimateClearingNextNotificationDelay; | |
108 notifications_.reset([[NSMutableArray alloc] init]); | |
109 notificationsPendingRemoval_.reset([[NSMutableArray alloc] init]); | |
110 } | |
111 return self; | |
112 } | |
113 | |
114 - (NSString*)trayTitle { | |
115 return [title_ stringValue]; | |
116 } | |
117 | |
118 - (void)setTrayTitle:(NSString*)title { | |
119 [title_ setStringValue:title]; | |
120 [title_ sizeToFit]; | |
121 } | |
122 | |
123 - (void)onWindowClosing { | |
124 if (animation_) { | |
125 [animation_ stopAnimation]; | |
126 [animation_ setDelegate:nil]; | |
127 animation_.reset(); | |
128 } | |
129 if (clearAllInProgress_) { | |
130 // To stop chain of clearOneNotification calls to start new animations. | |
131 [NSObject cancelPreviousPerformRequestsWithTarget:self]; | |
132 | |
133 for (NSViewAnimation* animation in clearAllAnimations_.get()) { | |
134 [animation stopAnimation]; | |
135 [animation setDelegate:nil]; | |
136 } | |
137 [clearAllAnimations_ removeAllObjects]; | |
138 [self finalizeClearAll]; | |
139 } | |
140 } | |
141 | |
142 - (void)loadView { | |
143 // Configure the root view as a background-colored box. | |
144 base::scoped_nsobject<NSBox> view([[NSBox alloc] initWithFrame:NSMakeRect( | |
145 0, 0, [MCTrayViewController trayWidth], kControlAreaHeight)]); | |
146 [view setBorderType:NSNoBorder]; | |
147 [view setBoxType:NSBoxCustom]; | |
148 [view setContentViewMargins:NSZeroSize]; | |
149 [view setFillColor:gfx::SkColorToCalibratedNSColor( | |
150 message_center::kMessageCenterBackgroundColor)]; | |
151 [view setTitlePosition:NSNoTitle]; | |
152 [view setWantsLayer:YES]; // Needed for notification view shadows. | |
153 [self setView:view]; | |
154 | |
155 [self layoutControlArea]; | |
156 | |
157 // Configure the scroll view in which all the notifications go. | |
158 base::scoped_nsobject<NSView> documentView( | |
159 [[NSView alloc] initWithFrame:NSZeroRect]); | |
160 scrollView_.reset([[NSScrollView alloc] initWithFrame:[view frame]]); | |
161 clipView_.reset( | |
162 [[MCClipView alloc] initWithFrame:[[scrollView_ contentView] frame]]); | |
163 [scrollView_ setContentView:clipView_]; | |
164 [scrollView_ setAutohidesScrollers:YES]; | |
165 [scrollView_ setAutoresizingMask:NSViewHeightSizable | NSViewMaxYMargin]; | |
166 [scrollView_ setDocumentView:documentView]; | |
167 [scrollView_ setDrawsBackground:NO]; | |
168 [scrollView_ setHasHorizontalScroller:NO]; | |
169 [scrollView_ setHasVerticalScroller:YES]; | |
170 [view addSubview:scrollView_]; | |
171 | |
172 [self onMessageCenterTrayChanged]; | |
173 } | |
174 | |
175 - (void)onMessageCenterTrayChanged { | |
176 if (settingsController_) | |
177 return [self updateTrayViewAndWindow]; | |
178 | |
179 std::map<std::string, MCNotificationController*> newMap; | |
180 | |
181 base::scoped_nsobject<NSShadow> shadow([[NSShadow alloc] init]); | |
182 [shadow setShadowColor:[NSColor colorWithDeviceWhite:0 alpha:0.55]]; | |
183 [shadow setShadowOffset:NSMakeSize(0, -1)]; | |
184 [shadow setShadowBlurRadius:2.0]; | |
185 | |
186 CGFloat minY = message_center::kMarginBetweenItems; | |
187 | |
188 // Iterate over the notifications in reverse, since the Cocoa coordinate | |
189 // origin is in the lower-left. Remove from |notificationsMap_| all the | |
190 // ones still in the updated model, so that those that should be removed | |
191 // will remain in the map. | |
192 const auto& modelNotifications = messageCenter_->GetVisibleNotifications(); | |
193 for (auto it = modelNotifications.rbegin(); | |
194 it != modelNotifications.rend(); | |
195 ++it) { | |
196 // Check if this notification is already in the tray. | |
197 const auto& existing = notificationsMap_.find((*it)->id()); | |
198 MCNotificationController* notification = nil; | |
199 if (existing == notificationsMap_.end()) { | |
200 base::scoped_nsobject<MCNotificationController> controller( | |
201 [[MCNotificationController alloc] | |
202 initWithNotification:*it | |
203 messageCenter:messageCenter_]); | |
204 [[controller view] setShadow:shadow]; | |
205 [[scrollView_ documentView] addSubview:[controller view]]; | |
206 | |
207 [notifications_ addObject:controller]; // Transfer ownership. | |
208 messageCenter_->DisplayedNotification( | |
209 (*it)->id(), message_center::DISPLAY_SOURCE_MESSAGE_CENTER); | |
210 | |
211 notification = controller.get(); | |
212 } else { | |
213 notification = existing->second; | |
214 [notification updateNotification:*it]; | |
215 notificationsMap_.erase(existing); | |
216 } | |
217 | |
218 DCHECK(notification); | |
219 | |
220 NSRect frame = [[notification view] frame]; | |
221 frame.origin.x = message_center::kMarginBetweenItems; | |
222 frame.origin.y = minY; | |
223 [[notification view] setFrame:frame]; | |
224 | |
225 newMap.insert(std::make_pair((*it)->id(), notification)); | |
226 | |
227 minY = NSMaxY(frame) + message_center::kMarginBetweenItems; | |
228 } | |
229 | |
230 // Remove any notifications that are no longer in the model. | |
231 for (const auto& pair : notificationsMap_) { | |
232 [[pair.second view] removeFromSuperview]; | |
233 [notifications_ removeObject:pair.second]; | |
234 } | |
235 | |
236 // Copy the new map of notifications to replace the old. | |
237 notificationsMap_ = newMap; | |
238 | |
239 [self updateTrayViewAndWindow]; | |
240 } | |
241 | |
242 - (void)toggleQuietMode:(id)sender { | |
243 if (messageCenter_->IsQuietMode()) | |
244 messageCenter_->SetQuietMode(false); | |
245 else | |
246 messageCenter_->EnterQuietModeWithExpire(base::TimeDelta::FromDays(1)); | |
247 | |
248 [self updateQuietModeButtonImage]; | |
249 } | |
250 | |
251 - (void)clearAllNotifications:(id)sender { | |
252 if ([self isAnimating]) { | |
253 clearAllDelayed_ = YES; | |
254 return; | |
255 } | |
256 | |
257 // Build a list for all notifications within the visible scroll range | |
258 // in preparation to slide them out one by one. | |
259 NSRect visibleScrollRect = [scrollView_ documentVisibleRect]; | |
260 for (MCNotificationController* notification in notifications_.get()) { | |
261 NSRect rect = [[notification view] frame]; | |
262 if (!NSIsEmptyRect(NSIntersectionRect(visibleScrollRect, rect))) { | |
263 visibleNotificationsPendingClear_.push_back(notification); | |
264 } | |
265 } | |
266 if (visibleNotificationsPendingClear_.empty()) | |
267 return; | |
268 | |
269 // Disbale buttons and freeze scroll bar to prevent the user from clicking on | |
270 // them accidentally. | |
271 [pauseButton_ setEnabled:NO]; | |
272 [clearAllButton_ setEnabled:NO]; | |
273 [settingsButton_ setEnabled:NO]; | |
274 [clipView_ setFrozen:YES]; | |
275 | |
276 // Start sliding out the top notification. | |
277 clearAllAnimations_.reset([[NSMutableArray alloc] init]); | |
278 [self clearOneNotification]; | |
279 | |
280 clearAllInProgress_ = YES; | |
281 } | |
282 | |
283 - (void)showSettings:(id)sender { | |
284 if (settingsController_) | |
285 return [self showMessages:sender]; | |
286 | |
287 message_center::NotifierSettingsProvider* provider = | |
288 messageCenter_->GetNotifierSettingsProvider(); | |
289 settingsController_.reset( | |
290 [[MCSettingsController alloc] initWithProvider:provider | |
291 trayViewController:self]); | |
292 | |
293 [[self view] addSubview:[settingsController_ view]]; | |
294 | |
295 NSRect titleFrame = [title_ frame]; | |
296 titleFrame.origin.x = | |
297 NSMaxX([backButton_ frame]) + message_center::kMarginBetweenItems / 2; | |
298 [title_ setFrame:titleFrame]; | |
299 [backButton_ setHidden:NO]; | |
300 [clearAllButton_ setEnabled:NO]; | |
301 | |
302 [scrollView_ setHidden:YES]; | |
303 | |
304 [[[self view] window] recalculateKeyViewLoop]; | |
305 messageCenter_->SetVisibility(message_center::VISIBILITY_SETTINGS); | |
306 | |
307 [self updateTrayViewAndWindow]; | |
308 } | |
309 | |
310 - (void)updateSettings { | |
311 // TODO(jianli): This class should not be calling -loadView, but instead | |
312 // should just observe a resize notification. | |
313 // (http://crbug.com/270251) | |
314 [[settingsController_ view] removeFromSuperview]; | |
315 [settingsController_ loadView]; | |
316 [[self view] addSubview:[settingsController_ view]]; | |
317 | |
318 [self updateTrayViewAndWindow]; | |
319 } | |
320 | |
321 - (void)showMessages:(id)sender { | |
322 messageCenter_->SetVisibility(message_center::VISIBILITY_MESSAGE_CENTER); | |
323 [self cleanupSettings]; | |
324 [[[self view] window] recalculateKeyViewLoop]; | |
325 [self updateTrayViewAndWindow]; | |
326 } | |
327 | |
328 - (void)cleanupSettings { | |
329 [scrollView_ setHidden:NO]; | |
330 | |
331 [[settingsController_ view] removeFromSuperview]; | |
332 settingsController_.reset(); | |
333 | |
334 NSRect titleFrame = [title_ frame]; | |
335 titleFrame.origin.x = NSMinX([backButton_ frame]); | |
336 [title_ setFrame:titleFrame]; | |
337 [backButton_ setHidden:YES]; | |
338 [clearAllButton_ setEnabled:YES]; | |
339 | |
340 } | |
341 | |
342 - (void)scrollToTop { | |
343 NSPoint topPoint = | |
344 NSMakePoint(0.0, [[scrollView_ documentView] bounds].size.height); | |
345 [[scrollView_ documentView] scrollPoint:topPoint]; | |
346 } | |
347 | |
348 - (BOOL)isAnimating { | |
349 return [animation_ isAnimating] || [clearAllAnimations_ count]; | |
350 } | |
351 | |
352 + (CGFloat)maxTrayClientHeight { | |
353 NSRect screenFrame = [[[NSScreen screens] objectAtIndex:0] visibleFrame]; | |
354 return NSHeight(screenFrame) - kTrayBottomMargin - kControlAreaHeight; | |
355 } | |
356 | |
357 + (CGFloat)trayWidth { | |
358 return message_center::kNotificationWidth + | |
359 2 * message_center::kMarginBetweenItems; | |
360 } | |
361 | |
362 // Testing API ///////////////////////////////////////////////////////////////// | |
363 | |
364 - (NSBox*)divider { | |
365 return divider_.get(); | |
366 } | |
367 | |
368 - (NSTextField*)emptyDescription { | |
369 return emptyDescription_.get(); | |
370 } | |
371 | |
372 - (NSScrollView*)scrollView { | |
373 return scrollView_.get(); | |
374 } | |
375 | |
376 - (HoverImageButton*)pauseButton { | |
377 return pauseButton_.get(); | |
378 } | |
379 | |
380 - (HoverImageButton*)clearAllButton { | |
381 return clearAllButton_.get(); | |
382 } | |
383 | |
384 - (void)setAnimationDuration:(NSTimeInterval)duration { | |
385 animationDuration_ = duration; | |
386 } | |
387 | |
388 - (void)setAnimateClearingNextNotificationDelay:(NSTimeInterval)delay { | |
389 animateClearingNextNotificationDelay_ = delay; | |
390 } | |
391 | |
392 - (void)setAnimationEndedCallback: | |
393 (message_center::TrayAnimationEndedCallback)callback { | |
394 testingAnimationEndedCallback_.reset(Block_copy(callback)); | |
395 } | |
396 | |
397 // Private ///////////////////////////////////////////////////////////////////// | |
398 | |
399 - (void)layoutControlArea { | |
400 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); | |
401 NSView* view = [self view]; | |
402 | |
403 // Create the "Notifications" label at the top of the tray. | |
404 NSFont* font = [NSFont labelFontOfSize:message_center::kTitleFontSize]; | |
405 NSColor* color = gfx::SkColorToCalibratedNSColor( | |
406 message_center::kMessageCenterBackgroundColor); | |
407 title_.reset( | |
408 [[MCTextField alloc] initWithFrame:NSZeroRect backgroundColor:color]); | |
409 | |
410 [title_ setFont:font]; | |
411 [title_ setStringValue: | |
412 l10n_util::GetNSString(IDS_MESSAGE_CENTER_FOOTER_TITLE)]; | |
413 [title_ setTextColor:gfx::SkColorToCalibratedNSColor( | |
414 message_center::kRegularTextColor)]; | |
415 [title_ sizeToFit]; | |
416 | |
417 NSRect titleFrame = [title_ frame]; | |
418 titleFrame.origin.x = message_center::kMarginBetweenItems; | |
419 titleFrame.origin.y = kControlAreaHeight/2 - NSMidY(titleFrame); | |
420 [title_ setFrame:titleFrame]; | |
421 [view addSubview:title_]; | |
422 | |
423 auto configureButton = ^(HoverImageButton* button) { | |
424 [[button cell] setHighlightsBy:NSOnState]; | |
425 [button setTrackingEnabled:YES]; | |
426 [button setBordered:NO]; | |
427 [button setAutoresizingMask:NSViewMinYMargin]; | |
428 [button setTarget:self]; | |
429 }; | |
430 | |
431 // Back button. On top of the "Notifications" label, hidden by default. | |
432 NSRect backButtonFrame = | |
433 NSMakeRect(NSMinX(titleFrame), | |
434 (kControlAreaHeight - kBackButtonSize) / 2, | |
435 kBackButtonSize, | |
436 kBackButtonSize); | |
437 backButton_.reset([[HoverImageButton alloc] initWithFrame:backButtonFrame]); | |
438 [backButton_ setDefaultImage: | |
439 rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW).ToNSImage()]; | |
440 [backButton_ setHoverImage: | |
441 rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW_HOVER).ToNSImage()]; | |
442 [backButton_ setPressedImage: | |
443 rb.GetNativeImageNamed(IDR_NOTIFICATION_ARROW_PRESSED).ToNSImage()]; | |
444 [backButton_ setAction:@selector(showMessages:)]; | |
445 configureButton(backButton_); | |
446 [backButton_ setHidden:YES]; | |
447 [backButton_ setKeyEquivalent:@"\e"]; | |
448 [backButton_ setToolTip:l10n_util::GetNSString( | |
449 IDS_MESSAGE_CENTER_SETTINGS_GO_BACK_BUTTON_TOOLTIP)]; | |
450 [[backButton_ cell] | |
451 accessibilitySetOverrideValue:[backButton_ toolTip] | |
452 forAttribute:NSAccessibilityDescriptionAttribute]; | |
453 [[self view] addSubview:backButton_]; | |
454 | |
455 // Create the divider line between the control area and the notifications. | |
456 divider_.reset( | |
457 [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, NSWidth([view frame]), 1)]); | |
458 [divider_ setAutoresizingMask:NSViewMinYMargin]; | |
459 [divider_ setBorderType:NSNoBorder]; | |
460 [divider_ setBoxType:NSBoxCustom]; | |
461 [divider_ setContentViewMargins:NSZeroSize]; | |
462 [divider_ setFillColor:gfx::SkColorToCalibratedNSColor( | |
463 message_center::kFooterDelimiterColor)]; | |
464 [divider_ setTitlePosition:NSNoTitle]; | |
465 [view addSubview:divider_]; | |
466 | |
467 | |
468 auto getButtonFrame = ^NSRect(CGFloat maxX, NSImage* image) { | |
469 NSSize size = [image size]; | |
470 return NSMakeRect( | |
471 maxX - size.width, | |
472 kControlAreaHeight/2 - size.height/2, | |
473 size.width, | |
474 size.height); | |
475 }; | |
476 | |
477 // Create the settings button at the far-right. | |
478 NSImage* defaultImage = | |
479 rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS).ToNSImage(); | |
480 NSRect settingsButtonFrame = getButtonFrame( | |
481 NSWidth([view frame]) - message_center::kMarginBetweenItems, | |
482 defaultImage); | |
483 settingsButton_.reset( | |
484 [[HoverImageButton alloc] initWithFrame:settingsButtonFrame]); | |
485 [settingsButton_ setDefaultImage:defaultImage]; | |
486 [settingsButton_ setHoverImage: | |
487 rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_HOVER).ToNSImage()]; | |
488 [settingsButton_ setPressedImage: | |
489 rb.GetNativeImageNamed(IDR_NOTIFICATION_SETTINGS_PRESSED).ToNSImage()]; | |
490 [settingsButton_ setToolTip: | |
491 l10n_util::GetNSString(IDS_MESSAGE_CENTER_SETTINGS_BUTTON_LABEL)]; | |
492 [[settingsButton_ cell] | |
493 accessibilitySetOverrideValue:[settingsButton_ toolTip] | |
494 forAttribute:NSAccessibilityDescriptionAttribute]; | |
495 [settingsButton_ setAction:@selector(showSettings:)]; | |
496 configureButton(settingsButton_); | |
497 [view addSubview:settingsButton_]; | |
498 | |
499 // Create the clear all button. | |
500 defaultImage = rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL).ToNSImage(); | |
501 NSRect clearAllButtonFrame = getButtonFrame( | |
502 NSMinX(settingsButtonFrame) - kButtonXMargin, | |
503 defaultImage); | |
504 clearAllButton_.reset( | |
505 [[HoverImageButton alloc] initWithFrame:clearAllButtonFrame]); | |
506 [clearAllButton_ setDefaultImage:defaultImage]; | |
507 [clearAllButton_ setHoverImage: | |
508 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_HOVER).ToNSImage()]; | |
509 [clearAllButton_ setPressedImage: | |
510 rb.GetNativeImageNamed(IDR_NOTIFICATION_CLEAR_ALL_PRESSED).ToNSImage()]; | |
511 [clearAllButton_ setToolTip: | |
512 l10n_util::GetNSString(IDS_MESSAGE_CENTER_CLEAR_ALL)]; | |
513 [[clearAllButton_ cell] | |
514 accessibilitySetOverrideValue:[clearAllButton_ toolTip] | |
515 forAttribute:NSAccessibilityDescriptionAttribute]; | |
516 [clearAllButton_ setAction:@selector(clearAllNotifications:)]; | |
517 configureButton(clearAllButton_); | |
518 [view addSubview:clearAllButton_]; | |
519 | |
520 // Create the pause button. | |
521 NSRect pauseButtonFrame = getButtonFrame( | |
522 NSMinX(clearAllButtonFrame) - kButtonXMargin, | |
523 defaultImage); | |
524 pauseButton_.reset([[HoverImageButton alloc] initWithFrame:pauseButtonFrame]); | |
525 [self updateQuietModeButtonImage]; | |
526 [pauseButton_ setHoverImage: rb.GetNativeImageNamed( | |
527 IDR_NOTIFICATION_DO_NOT_DISTURB_HOVER).ToNSImage()]; | |
528 [pauseButton_ setToolTip: | |
529 l10n_util::GetNSString(IDS_MESSAGE_CENTER_QUIET_MODE_BUTTON_TOOLTIP)]; | |
530 [[pauseButton_ cell] | |
531 accessibilitySetOverrideValue:[pauseButton_ toolTip] | |
532 forAttribute:NSAccessibilityDescriptionAttribute]; | |
533 [pauseButton_ setAction:@selector(toggleQuietMode:)]; | |
534 configureButton(pauseButton_); | |
535 [view addSubview:pauseButton_]; | |
536 | |
537 // Create the description field for the empty message center. Initially it is | |
538 // invisible. | |
539 emptyDescription_.reset( | |
540 [[MCTextField alloc] initWithFrame:NSZeroRect backgroundColor:color]); | |
541 | |
542 NSFont* smallFont = | |
543 [NSFont labelFontOfSize:message_center::kEmptyCenterFontSize]; | |
544 [emptyDescription_ setFont:smallFont]; | |
545 [emptyDescription_ setStringValue: | |
546 l10n_util::GetNSString(IDS_MESSAGE_CENTER_NO_MESSAGES)]; | |
547 [emptyDescription_ setTextColor:gfx::SkColorToCalibratedNSColor( | |
548 message_center::kDimTextColor)]; | |
549 [emptyDescription_ sizeToFit]; | |
550 [emptyDescription_ setHidden:YES]; | |
551 | |
552 [view addSubview:emptyDescription_]; | |
553 } | |
554 | |
555 - (void)updateTrayViewAndWindow { | |
556 CGFloat scrollContentHeight = message_center::kMinScrollViewHeight; | |
557 if ([notifications_ count]) { | |
558 [emptyDescription_ setHidden:YES]; | |
559 [scrollView_ setHidden:NO]; | |
560 [divider_ setHidden:NO]; | |
561 scrollContentHeight = NSMaxY([[[notifications_ lastObject] view] frame]) + | |
562 message_center::kMarginBetweenItems;; | |
563 } else { | |
564 [emptyDescription_ setHidden:NO]; | |
565 [scrollView_ setHidden:YES]; | |
566 [divider_ setHidden:YES]; | |
567 | |
568 NSRect centeredFrame = [emptyDescription_ frame]; | |
569 NSPoint centeredOrigin = NSMakePoint( | |
570 floor((NSWidth([[self view] frame]) - NSWidth(centeredFrame))/2 + 0.5), | |
571 floor((scrollContentHeight - NSHeight(centeredFrame))/2 + 0.5)); | |
572 | |
573 centeredFrame.origin = centeredOrigin; | |
574 [emptyDescription_ setFrame:centeredFrame]; | |
575 } | |
576 | |
577 // Resize the scroll view's content. | |
578 NSRect scrollViewFrame = [scrollView_ frame]; | |
579 NSRect documentFrame = [[scrollView_ documentView] frame]; | |
580 documentFrame.size.width = NSWidth(scrollViewFrame); | |
581 documentFrame.size.height = scrollContentHeight; | |
582 [[scrollView_ documentView] setFrame:documentFrame]; | |
583 | |
584 // Resize the container view. | |
585 NSRect frame = [[self view] frame]; | |
586 CGFloat oldHeight = NSHeight(frame); | |
587 if (settingsController_) { | |
588 frame.size.height = NSHeight([[settingsController_ view] frame]); | |
589 } else { | |
590 frame.size.height = std::min([MCTrayViewController maxTrayClientHeight], | |
591 scrollContentHeight); | |
592 } | |
593 frame.size.height += kControlAreaHeight; | |
594 CGFloat newHeight = NSHeight(frame); | |
595 [[self view] setFrame:frame]; | |
596 | |
597 // Resize the scroll view. | |
598 scrollViewFrame.size.height = NSHeight(frame) - kControlAreaHeight; | |
599 [scrollView_ setFrame:scrollViewFrame]; | |
600 | |
601 // Resize the window. | |
602 NSRect windowFrame = [[[self view] window] frame]; | |
603 CGFloat delta = newHeight - oldHeight; | |
604 windowFrame.origin.y -= delta; | |
605 windowFrame.size.height += delta; | |
606 | |
607 [[[self view] window] setFrame:windowFrame display:YES]; | |
608 // Hide the clear-all button if there are no notifications. Simply swap the | |
609 // X position of it and the pause button in that case. | |
610 BOOL hidden = ![notifications_ count]; | |
611 if ([clearAllButton_ isHidden] != hidden) { | |
612 [clearAllButton_ setHidden:hidden]; | |
613 | |
614 NSRect pauseButtonFrame = [pauseButton_ frame]; | |
615 NSRect clearAllButtonFrame = [clearAllButton_ frame]; | |
616 std::swap(clearAllButtonFrame.origin.x, pauseButtonFrame.origin.x); | |
617 [pauseButton_ setFrame:pauseButtonFrame]; | |
618 [clearAllButton_ setFrame:clearAllButtonFrame]; | |
619 } | |
620 } | |
621 | |
622 - (void)animationDidEnd:(NSAnimation*)animation { | |
623 if (clearAllInProgress_) { | |
624 // For clear-all animation. | |
625 [clearAllAnimations_ removeObject:animation]; | |
626 if (![clearAllAnimations_ count] && | |
627 visibleNotificationsPendingClear_.empty()) { | |
628 [self finalizeClearAll]; | |
629 } | |
630 } else { | |
631 // For notification removal and reposition animation. | |
632 if ([notificationsPendingRemoval_ count]) { | |
633 [self moveUpRemainingNotifications]; | |
634 } else { | |
635 [self finalizeTrayViewAndWindow]; | |
636 | |
637 if (clearAllDelayed_) | |
638 [self clearAllNotifications:nil]; | |
639 } | |
640 } | |
641 | |
642 // Give the testing code a chance to do something, i.e. quitting the test | |
643 // run loop. | |
644 if (![self isAnimating] && testingAnimationEndedCallback_) | |
645 testingAnimationEndedCallback_.get()(); | |
646 } | |
647 | |
648 - (void)closeNotificationsByUser { | |
649 // No need to close individual notification if clear-all is in progress. | |
650 if (clearAllInProgress_) | |
651 return; | |
652 | |
653 if ([self isAnimating]) | |
654 return; | |
655 [self hideNotificationsPendingRemoval]; | |
656 } | |
657 | |
658 - (void)hideNotificationsPendingRemoval { | |
659 base::scoped_nsobject<NSMutableArray> animationDataArray( | |
660 [[NSMutableArray alloc] init]); | |
661 | |
662 // Fade-out those notifications pending removal. | |
663 for (MCNotificationController* notification in notifications_.get()) { | |
664 if (messageCenter_->FindVisibleNotificationById( | |
665 [notification notificationID])) | |
666 continue; | |
667 [notificationsPendingRemoval_ addObject:notification]; | |
668 [animationDataArray addObject:@{ | |
669 NSViewAnimationTargetKey : [notification view], | |
670 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect | |
671 }]; | |
672 } | |
673 | |
674 if ([notificationsPendingRemoval_ count] == 0) | |
675 return; | |
676 | |
677 for (MCNotificationController* notification in | |
678 notificationsPendingRemoval_.get()) { | |
679 [notifications_ removeObject:notification]; | |
680 } | |
681 | |
682 // Start the animation. | |
683 animation_.reset([[NSViewAnimation alloc] | |
684 initWithViewAnimations:animationDataArray]); | |
685 [animation_ setDuration:animationDuration_]; | |
686 [animation_ setDelegate:self]; | |
687 [animation_ startAnimation]; | |
688 } | |
689 | |
690 - (void)moveUpRemainingNotifications { | |
691 base::scoped_nsobject<NSMutableArray> animationDataArray( | |
692 [[NSMutableArray alloc] init]); | |
693 | |
694 // Compute the position where the remaining notifications should start. | |
695 CGFloat minY = message_center::kMarginBetweenItems; | |
696 for (MCNotificationController* notification in | |
697 notificationsPendingRemoval_.get()) { | |
698 NSView* view = [notification view]; | |
699 minY += NSHeight([view frame]) + message_center::kMarginBetweenItems; | |
700 } | |
701 | |
702 // Reposition the remaining notifications starting at the computed position. | |
703 for (MCNotificationController* notification in notifications_.get()) { | |
704 NSView* view = [notification view]; | |
705 NSRect frame = [view frame]; | |
706 NSRect oldFrame = frame; | |
707 frame.origin.y = minY; | |
708 if (!NSEqualRects(oldFrame, frame)) { | |
709 [animationDataArray addObject:@{ | |
710 NSViewAnimationTargetKey : view, | |
711 NSViewAnimationEndFrameKey : [NSValue valueWithRect:frame] | |
712 }]; | |
713 } | |
714 minY = NSMaxY(frame) + message_center::kMarginBetweenItems; | |
715 } | |
716 | |
717 // Now remove notifications pending removal. | |
718 for (MCNotificationController* notification in | |
719 notificationsPendingRemoval_.get()) { | |
720 [[notification view] removeFromSuperview]; | |
721 notificationsMap_.erase([notification notificationID]); | |
722 } | |
723 [notificationsPendingRemoval_ removeAllObjects]; | |
724 | |
725 // Start the animation. | |
726 animation_.reset([[NSViewAnimation alloc] | |
727 initWithViewAnimations:animationDataArray]); | |
728 [animation_ setDuration:animationDuration_]; | |
729 [animation_ setDelegate:self]; | |
730 [animation_ startAnimation]; | |
731 } | |
732 | |
733 - (void)finalizeTrayViewAndWindow { | |
734 // Reposition the remaining notifications starting at the bottom. | |
735 CGFloat minY = message_center::kMarginBetweenItems; | |
736 for (MCNotificationController* notification in notifications_.get()) { | |
737 NSView* view = [notification view]; | |
738 NSRect frame = [view frame]; | |
739 NSRect oldFrame = frame; | |
740 frame.origin.y = minY; | |
741 if (!NSEqualRects(oldFrame, frame)) | |
742 [view setFrame:frame]; | |
743 minY = NSMaxY(frame) + message_center::kMarginBetweenItems; | |
744 } | |
745 | |
746 [self updateTrayViewAndWindow]; | |
747 | |
748 // Check if there're more notifications pending removal. | |
749 [self closeNotificationsByUser]; | |
750 } | |
751 | |
752 - (void)clearOneNotification { | |
753 DCHECK(!visibleNotificationsPendingClear_.empty()); | |
754 | |
755 MCNotificationController* notification = | |
756 visibleNotificationsPendingClear_.back(); | |
757 visibleNotificationsPendingClear_.pop_back(); | |
758 | |
759 // Slide out the notification from left to right with fade-out simultaneously. | |
760 NSRect newFrame = [[notification view] frame]; | |
761 newFrame.origin.x = NSMaxX(newFrame) + message_center::kMarginBetweenItems; | |
762 NSDictionary* animationDict = @{ | |
763 NSViewAnimationTargetKey : [notification view], | |
764 NSViewAnimationEndFrameKey : [NSValue valueWithRect:newFrame], | |
765 NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect | |
766 }; | |
767 base::scoped_nsobject<NSViewAnimation> animation([[NSViewAnimation alloc] | |
768 initWithViewAnimations:[NSArray arrayWithObject:animationDict]]); | |
769 [animation setDuration:animationDuration_]; | |
770 [animation setDelegate:self]; | |
771 [animation startAnimation]; | |
772 [clearAllAnimations_ addObject:animation]; | |
773 | |
774 // Schedule to start sliding out next notification after a short delay. | |
775 if (!visibleNotificationsPendingClear_.empty()) { | |
776 [self performSelector:@selector(clearOneNotification) | |
777 withObject:nil | |
778 afterDelay:animateClearingNextNotificationDelay_]; | |
779 } | |
780 } | |
781 | |
782 - (void)finalizeClearAll { | |
783 DCHECK(clearAllInProgress_); | |
784 clearAllInProgress_ = NO; | |
785 | |
786 DCHECK(![clearAllAnimations_ count]); | |
787 clearAllAnimations_.reset(); | |
788 | |
789 [pauseButton_ setEnabled:YES]; | |
790 [clearAllButton_ setEnabled:YES]; | |
791 [settingsButton_ setEnabled:YES]; | |
792 [clipView_ setFrozen:NO]; | |
793 | |
794 messageCenter_->RemoveAllVisibleNotifications(true); | |
795 } | |
796 | |
797 - (void)updateQuietModeButtonImage { | |
798 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); | |
799 if (messageCenter_->IsQuietMode()) { | |
800 [pauseButton_ setTrackingEnabled:NO]; | |
801 [pauseButton_ setDefaultImage: rb.GetNativeImageNamed( | |
802 IDR_NOTIFICATION_DO_NOT_DISTURB_PRESSED).ToNSImage()]; | |
803 } else { | |
804 [pauseButton_ setTrackingEnabled:YES]; | |
805 [pauseButton_ setDefaultImage: | |
806 rb.GetNativeImageNamed(IDR_NOTIFICATION_DO_NOT_DISTURB).ToNSImage()]; | |
807 } | |
808 } | |
809 | |
810 @end | |
OLD | NEW |