• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/popup_controller.h"
6
7#include <cmath>
8
9#import "base/mac/foundation_util.h"
10#import "base/mac/sdk_forward_declarations.h"
11#import "ui/base/cocoa/window_size_constants.h"
12#import "ui/message_center/cocoa/notification_controller.h"
13#import "ui/message_center/cocoa/popup_collection.h"
14#include "ui/message_center/message_center.h"
15
16////////////////////////////////////////////////////////////////////////////////
17
18@interface MCPopupController (Private)
19- (void)notificationSwipeStarted;
20- (void)notificationSwipeMoved:(CGFloat)amount;
21- (void)notificationSwipeEnded:(BOOL)ended complete:(BOOL)isComplete;
22@end
23
24// Window Subclass /////////////////////////////////////////////////////////////
25
26@interface MCPopupWindow : NSPanel {
27  // The cumulative X and Y scrollingDeltas since the -scrollWheel: event began.
28  NSPoint totalScrollDelta_;
29}
30@end
31
32@implementation MCPopupWindow
33
34- (void)scrollWheel:(NSEvent*)event {
35  // Gesture swiping only exists on 10.7+.
36  if (![event respondsToSelector:@selector(phase)])
37    return;
38
39  NSEventPhase phase = [event phase];
40  BOOL shouldTrackSwipe = NO;
41
42  if (phase == NSEventPhaseBegan) {
43    totalScrollDelta_ = NSZeroPoint;
44  } else if (phase == NSEventPhaseChanged) {
45    shouldTrackSwipe = YES;
46    totalScrollDelta_.x += [event scrollingDeltaX];
47    totalScrollDelta_.y += [event scrollingDeltaY];
48  }
49
50  // Only allow horizontal scrolling.
51  if (std::abs(totalScrollDelta_.x) < std::abs(totalScrollDelta_.y))
52    return;
53
54  if (shouldTrackSwipe) {
55    MCPopupController* controller =
56        base::mac::ObjCCastStrict<MCPopupController>([self windowController]);
57    BOOL directionInverted = [event isDirectionInvertedFromDevice];
58
59    auto handler = ^(CGFloat gestureAmount, NSEventPhase phase,
60                     BOOL isComplete, BOOL* stop) {
61        // The swipe direction should match the direction the user's fingers
62        // are moving, not the interpreted scroll direction.
63        if (directionInverted)
64          gestureAmount *= -1;
65
66        if (phase == NSEventPhaseBegan) {
67          [controller notificationSwipeStarted];
68          return;
69        }
70
71        [controller notificationSwipeMoved:gestureAmount];
72
73        BOOL ended = phase == NSEventPhaseEnded;
74        if (ended || isComplete)
75          [controller notificationSwipeEnded:ended complete:isComplete];
76    };
77    [event trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection
78             dampenAmountThresholdMin:-1
79                                  max:1
80                         usingHandler:handler];
81  }
82}
83
84@end
85
86////////////////////////////////////////////////////////////////////////////////
87
88@implementation MCPopupController
89
90- (id)initWithNotification:(const message_center::Notification*)notification
91             messageCenter:(message_center::MessageCenter*)messageCenter
92           popupCollection:(MCPopupCollection*)popupCollection {
93  base::scoped_nsobject<MCPopupWindow> window(
94      [[MCPopupWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater
95                                       styleMask:NSBorderlessWindowMask |
96                                                 NSNonactivatingPanelMask
97                                         backing:NSBackingStoreBuffered
98                                           defer:YES]);
99  if ((self = [super initWithWindow:window])) {
100    messageCenter_ = messageCenter;
101    popupCollection_ = popupCollection;
102    notificationController_.reset(
103        [[MCNotificationController alloc] initWithNotification:notification
104                                                 messageCenter:messageCenter_]);
105    isClosing_ = NO;
106    bounds_ = [[notificationController_ view] frame];
107
108    [window setReleasedWhenClosed:NO];
109
110    [window setLevel:NSFloatingWindowLevel];
111    [window setExcludedFromWindowsMenu:YES];
112    [window setCollectionBehavior:
113        NSWindowCollectionBehaviorIgnoresCycle |
114        NSWindowCollectionBehaviorFullScreenAuxiliary];
115
116    [window setHasShadow:YES];
117    [window setContentView:[notificationController_ view]];
118
119    trackingArea_.reset(
120        [[CrTrackingArea alloc] initWithRect:NSZeroRect
121                                     options:NSTrackingInVisibleRect |
122                                             NSTrackingMouseEnteredAndExited |
123                                             NSTrackingActiveAlways
124                                       owner:self
125                                    userInfo:nil]);
126    [[window contentView] addTrackingArea:trackingArea_.get()];
127  }
128  return self;
129}
130
131- (void)close {
132  if (boundsAnimation_) {
133    [boundsAnimation_ stopAnimation];
134    [boundsAnimation_ setDelegate:nil];
135    boundsAnimation_.reset();
136  }
137  if (trackingArea_.get())
138    [[[self window] contentView] removeTrackingArea:trackingArea_.get()];
139  [super close];
140  [self performSelectorOnMainThread:@selector(release)
141                         withObject:nil
142                      waitUntilDone:NO
143                              modes:@[ NSDefaultRunLoopMode ]];
144}
145
146- (MCNotificationController*)notificationController {
147  return notificationController_.get();
148}
149
150- (const message_center::Notification*)notification {
151  return [notificationController_ notification];
152}
153
154- (const std::string&)notificationID {
155  return [notificationController_ notificationID];
156}
157
158// Private /////////////////////////////////////////////////////////////////////
159
160- (void)notificationSwipeStarted {
161  originalFrame_ = [[self window] frame];
162  swipeGestureEnded_ = NO;
163}
164
165- (void)notificationSwipeMoved:(CGFloat)amount {
166  NSWindow* window = [self window];
167
168  [window setAlphaValue:1.0 - std::abs(amount)];
169  NSRect frame = [window frame];
170  CGFloat originalMin = NSMinX(originalFrame_);
171  frame.origin.x = originalMin + (NSMidX(originalFrame_) - originalMin) *
172                   -amount;
173  [window setFrame:frame display:YES];
174}
175
176- (void)notificationSwipeEnded:(BOOL)ended complete:(BOOL)isComplete {
177  swipeGestureEnded_ |= ended;
178  if (swipeGestureEnded_ && isComplete) {
179    messageCenter_->RemoveNotification([self notificationID], /*by_user=*/true);
180    [popupCollection_ onPopupAnimationEnded:[self notificationID]];
181  }
182}
183
184- (void)animationDidEnd:(NSAnimation*)animation {
185  if (animation != boundsAnimation_.get())
186    return;
187  boundsAnimation_.reset();
188
189  [popupCollection_ onPopupAnimationEnded:[self notificationID]];
190
191  if (isClosing_)
192    [self close];
193}
194
195- (void)showWithAnimation:(NSRect)newBounds {
196  bounds_ = newBounds;
197  NSRect startBounds = newBounds;
198  startBounds.origin.x += startBounds.size.width;
199  [[self window] setFrame:startBounds display:NO];
200  [[self window] setAlphaValue:0];
201  [self showWindow:nil];
202
203  // Slide-in and fade-in simultaneously.
204  NSDictionary* animationDict = @{
205    NSViewAnimationTargetKey : [self window],
206    NSViewAnimationEndFrameKey : [NSValue valueWithRect:newBounds],
207    NSViewAnimationEffectKey : NSViewAnimationFadeInEffect
208  };
209  DCHECK(!boundsAnimation_);
210  boundsAnimation_.reset([[NSViewAnimation alloc]
211      initWithViewAnimations:[NSArray arrayWithObject:animationDict]]);
212  [boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]];
213  [boundsAnimation_ setDelegate:self];
214  [boundsAnimation_ startAnimation];
215}
216
217- (void)closeWithAnimation {
218  if (isClosing_)
219    return;
220
221  isClosing_ = YES;
222
223  // If the notification was swiped closed, do not animate it as the
224  // notification has already faded out.
225  if (swipeGestureEnded_) {
226    [self close];
227    return;
228  }
229
230  NSDictionary* animationDict = @{
231    NSViewAnimationTargetKey : [self window],
232    NSViewAnimationEffectKey : NSViewAnimationFadeOutEffect
233  };
234  DCHECK(!boundsAnimation_);
235  boundsAnimation_.reset([[NSViewAnimation alloc]
236      initWithViewAnimations:[NSArray arrayWithObject:animationDict]]);
237  [boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]];
238  [boundsAnimation_ setDelegate:self];
239  [boundsAnimation_ startAnimation];
240}
241
242- (void)markPopupCollectionGone {
243  popupCollection_ = nil;
244}
245
246- (NSRect)bounds {
247  return bounds_;
248}
249
250- (void)setBounds:(NSRect)newBounds {
251  if (isClosing_ || NSEqualRects(bounds_ , newBounds))
252    return;
253  bounds_ = newBounds;
254
255  NSDictionary* animationDict = @{
256    NSViewAnimationTargetKey :   [self window],
257    NSViewAnimationEndFrameKey : [NSValue valueWithRect:newBounds]
258  };
259  DCHECK(!boundsAnimation_);
260  boundsAnimation_.reset([[NSViewAnimation alloc]
261      initWithViewAnimations:[NSArray arrayWithObject:animationDict]]);
262  [boundsAnimation_ setDuration:[popupCollection_ popupAnimationDuration]];
263  [boundsAnimation_ setDelegate:self];
264  [boundsAnimation_ startAnimation];
265}
266
267- (void)mouseEntered:(NSEvent*)event {
268  messageCenter_->PausePopupTimers();
269}
270
271- (void)mouseExited:(NSEvent*)event {
272  messageCenter_->RestartPopupTimers();
273}
274
275@end
276