Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / confirm_quit_panel_controller.mm
blob499dc931ece8f76767c376dfec4a4227eb6a71de
1 // Copyright (c) 2012 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 #import <Cocoa/Cocoa.h>
6 #import <QuartzCore/QuartzCore.h>
8 #import "chrome/browser/ui/cocoa/confirm_quit_panel_controller.h"
10 #include "base/logging.h"
11 #include "base/mac/scoped_nsobject.h"
12 #include "base/metrics/histogram.h"
13 #include "base/prefs/pref_registry_simple.h"
14 #include "base/strings/sys_string_conversions.h"
15 #include "chrome/browser/browser_process.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/profiles/profile_manager.h"
18 #include "chrome/browser/ui/cocoa/confirm_quit.h"
19 #include "chrome/common/pref_names.h"
20 #include "grit/generated_resources.h"
21 #import "ui/base/accelerators/platform_accelerator_cocoa.h"
22 #include "ui/base/l10n/l10n_util_mac.h"
24 // Constants ///////////////////////////////////////////////////////////////////
26 // How long the user must hold down Cmd+Q to confirm the quit.
27 const NSTimeInterval kTimeToConfirmQuit = 1.5;
29 // Leeway between the |targetDate| and the current time that will confirm a
30 // quit.
31 const NSTimeInterval kTimeDeltaFuzzFactor = 1.0;
33 // Duration of the window fade out animation.
34 const NSTimeInterval kWindowFadeAnimationDuration = 0.2;
36 // For metrics recording only: How long the user must hold the keys to
37 // differentitate kDoubleTap from kTapHold.
38 const NSTimeInterval kDoubleTapTimeDelta = 0.32;
40 // Functions ///////////////////////////////////////////////////////////////////
42 namespace confirm_quit {
44 void RecordHistogram(ConfirmQuitMetric sample) {
45   UMA_HISTOGRAM_ENUMERATION("OSX.ConfirmToQuit", sample, kSampleCount);
48 void RegisterLocalState(PrefRegistrySimple* registry) {
49   registry->RegisterBooleanPref(prefs::kConfirmToQuitEnabled, false);
52 }  // namespace confirm_quit
54 // Custom Content View /////////////////////////////////////////////////////////
56 // The content view of the window that draws a custom frame.
57 @interface ConfirmQuitFrameView : NSView {
58  @private
59   NSTextField* message_;  // Weak, owned by the view hierarchy.
61 - (void)setMessageText:(NSString*)text;
62 @end
64 @implementation ConfirmQuitFrameView
66 - (id)initWithFrame:(NSRect)frameRect {
67   if ((self = [super initWithFrame:frameRect])) {
68     base::scoped_nsobject<NSTextField> message(
69         // The frame will be fixed up when |-setMessageText:| is called.
70         [[NSTextField alloc] initWithFrame:NSZeroRect]);
71     message_ = message.get();
72     [message_ setEditable:NO];
73     [message_ setSelectable:NO];
74     [message_ setBezeled:NO];
75     [message_ setDrawsBackground:NO];
76     [message_ setFont:[NSFont boldSystemFontOfSize:24]];
77     [message_ setTextColor:[NSColor whiteColor]];
78     [self addSubview:message_];
79   }
80   return self;
83 - (void)drawRect:(NSRect)dirtyRect {
84   const CGFloat kCornerRadius = 5.0;
85   NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:[self bounds]
86                                                        xRadius:kCornerRadius
87                                                        yRadius:kCornerRadius];
89   NSColor* fillColor = [NSColor colorWithCalibratedWhite:0.2 alpha:0.75];
90   [fillColor set];
91   [path fill];
94 - (void)setMessageText:(NSString*)text {
95   const CGFloat kHorizontalPadding = 30;  // In view coordinates.
97   // Style the string.
98   base::scoped_nsobject<NSMutableAttributedString> attrString(
99       [[NSMutableAttributedString alloc] initWithString:text]);
100   base::scoped_nsobject<NSShadow> textShadow([[NSShadow alloc] init]);
101   [textShadow.get() setShadowColor:[NSColor colorWithCalibratedWhite:0
102                                                                alpha:0.6]];
103   [textShadow.get() setShadowOffset:NSMakeSize(0, -1)];
104   [textShadow setShadowBlurRadius:1.0];
105   [attrString addAttribute:NSShadowAttributeName
106                      value:textShadow
107                      range:NSMakeRange(0, [text length])];
108   [message_ setAttributedStringValue:attrString];
110   // Fixup the frame of the string.
111   [message_ sizeToFit];
112   NSRect messageFrame = [message_ frame];
113   NSRect frameInViewSpace =
114       [message_ convertRect:[[self window] frame] fromView:nil];
116   if (NSWidth(messageFrame) > NSWidth(frameInViewSpace))
117     frameInViewSpace.size.width = NSWidth(messageFrame) + kHorizontalPadding;
119   messageFrame.origin.x = NSWidth(frameInViewSpace) / 2 - NSMidX(messageFrame);
120   messageFrame.origin.y = NSHeight(frameInViewSpace) / 2 - NSMidY(messageFrame);
122   [[self window] setFrame:[message_ convertRect:frameInViewSpace toView:nil]
123                   display:YES];
124   [message_ setFrame:messageFrame];
127 @end
129 // Animation ///////////////////////////////////////////////////////////////////
131 // This animation will run through all the windows of the passed-in
132 // NSApplication and will fade their alpha value to 0.0. When the animation is
133 // complete, this will release itself.
134 @interface FadeAllWindowsAnimation : NSAnimation<NSAnimationDelegate> {
135  @private
136   NSApplication* application_;
138 - (id)initWithApplication:(NSApplication*)app
139         animationDuration:(NSTimeInterval)duration;
140 @end
143 @implementation FadeAllWindowsAnimation
145 - (id)initWithApplication:(NSApplication*)app
146         animationDuration:(NSTimeInterval)duration {
147   if ((self = [super initWithDuration:duration
148                        animationCurve:NSAnimationLinear])) {
149     application_ = app;
150     [self setDelegate:self];
151   }
152   return self;
155 - (void)setCurrentProgress:(NSAnimationProgress)progress {
156   for (NSWindow* window in [application_ windows]) {
157     [window setAlphaValue:1.0 - progress];
158   }
161 - (void)animationDidStop:(NSAnimation*)anim {
162   DCHECK_EQ(self, anim);
163   [self autorelease];
166 @end
168 // Private Interface ///////////////////////////////////////////////////////////
170 @interface ConfirmQuitPanelController (Private)
171 - (void)animateFadeOut;
172 - (NSEvent*)pumpEventQueueForKeyUp:(NSApplication*)app untilDate:(NSDate*)date;
173 - (void)hideAllWindowsForApplication:(NSApplication*)app
174                         withDuration:(NSTimeInterval)duration;
175 // Returns the Accelerator for the Quit menu item.
176 + (scoped_ptr<ui::PlatformAcceleratorCocoa>)quitAccelerator;
177 @end
179 ConfirmQuitPanelController* g_confirmQuitPanelController = nil;
181 ////////////////////////////////////////////////////////////////////////////////
183 @implementation ConfirmQuitPanelController
185 + (ConfirmQuitPanelController*)sharedController {
186   if (!g_confirmQuitPanelController) {
187     g_confirmQuitPanelController =
188         [[ConfirmQuitPanelController alloc] init];
189   }
190   return [[g_confirmQuitPanelController retain] autorelease];
193 - (id)init {
194   const NSRect kWindowFrame = NSMakeRect(0, 0, 350, 70);
195   base::scoped_nsobject<NSWindow> window(
196       [[NSWindow alloc] initWithContentRect:kWindowFrame
197                                   styleMask:NSBorderlessWindowMask
198                                     backing:NSBackingStoreBuffered
199                                       defer:NO]);
200   if ((self = [super initWithWindow:window])) {
201     [window setDelegate:self];
202     [window setBackgroundColor:[NSColor clearColor]];
203     [window setOpaque:NO];
204     [window setHasShadow:NO];
206     // Create the content view. Take the frame from the existing content view.
207     NSRect frame = [[window contentView] frame];
208     base::scoped_nsobject<ConfirmQuitFrameView> frameView(
209         [[ConfirmQuitFrameView alloc] initWithFrame:frame]);
210     contentView_ = frameView.get();
211     [window setContentView:contentView_];
213     // Set the proper string.
214     NSString* message = l10n_util::GetNSStringF(IDS_CONFIRM_TO_QUIT_DESCRIPTION,
215         base::SysNSStringToUTF16([[self class] keyCommandString]));
216     [contentView_ setMessageText:message];
217   }
218   return self;
221 + (BOOL)eventTriggersFeature:(NSEvent*)event {
222   if ([event type] != NSKeyDown)
223     return NO;
224   ui::PlatformAcceleratorCocoa eventAccelerator(
225       [event charactersIgnoringModifiers],
226       [event modifierFlags] & NSDeviceIndependentModifierFlagsMask);
227   scoped_ptr<ui::PlatformAcceleratorCocoa> quitAccelerator(
228       [self quitAccelerator]);
229   return quitAccelerator->Equals(eventAccelerator);
232 - (NSApplicationTerminateReply)runModalLoopForApplication:(NSApplication*)app {
233   base::scoped_nsobject<ConfirmQuitPanelController> keepAlive([self retain]);
235   // If this is the second of two such attempts to quit within a certain time
236   // interval, then just quit.
237   // Time of last quit attempt, if any.
238   static NSDate* lastQuitAttempt;  // Initially nil, as it's static.
239   NSDate* timeNow = [NSDate date];
240   if (lastQuitAttempt &&
241       [timeNow timeIntervalSinceDate:lastQuitAttempt] < kTimeDeltaFuzzFactor) {
242     // The panel tells users to Hold Cmd+Q. However, we also want to have a
243     // double-tap shortcut that allows for a quick quit path. For the users who
244     // tap Cmd+Q and then hold it with the window still open, this double-tap
245     // logic will run and cause the quit to get committed. If the key
246     // combination held down, the system will start sending the Cmd+Q event to
247     // the next key application, and so on. This is bad, so instead we hide all
248     // the windows (without animation) to look like we've "quit" and then wait
249     // for the KeyUp event to commit the quit.
250     [self hideAllWindowsForApplication:app withDuration:0];
251     NSEvent* nextEvent = [self pumpEventQueueForKeyUp:app
252                                             untilDate:[NSDate distantFuture]];
253     [app discardEventsMatchingMask:NSAnyEventMask beforeEvent:nextEvent];
255     // Based on how long the user held the keys, record the metric.
256     if ([[NSDate date] timeIntervalSinceDate:timeNow] < kDoubleTapTimeDelta)
257       confirm_quit::RecordHistogram(confirm_quit::kDoubleTap);
258     else
259       confirm_quit::RecordHistogram(confirm_quit::kTapHold);
260     return NSTerminateNow;
261   } else {
262     [lastQuitAttempt release];  // Harmless if already nil.
263     lastQuitAttempt = [timeNow retain];  // Record this attempt for next time.
264   }
266   // Show the info panel that explains what the user must to do confirm quit.
267   [self showWindow:self];
269   // Spin a nested run loop until the |targetDate| is reached or a KeyUp event
270   // is sent.
271   NSDate* targetDate = [NSDate dateWithTimeIntervalSinceNow:kTimeToConfirmQuit];
272   BOOL willQuit = NO;
273   NSEvent* nextEvent = nil;
274   do {
275     // Dequeue events until a key up is received. To avoid busy waiting, figure
276     // out the amount of time that the thread can sleep before taking further
277     // action.
278     NSDate* waitDate = [NSDate dateWithTimeIntervalSinceNow:
279         kTimeToConfirmQuit - kTimeDeltaFuzzFactor];
280     nextEvent = [self pumpEventQueueForKeyUp:app untilDate:waitDate];
282     // Wait for the time expiry to happen. Once past the hold threshold,
283     // commit to quitting and hide all the open windows.
284     if (!willQuit) {
285       NSDate* now = [NSDate date];
286       NSTimeInterval difference = [targetDate timeIntervalSinceDate:now];
287       if (difference < kTimeDeltaFuzzFactor) {
288         willQuit = YES;
290         // At this point, the quit has been confirmed and windows should all
291         // fade out to convince the user to release the key combo to finalize
292         // the quit.
293         [self hideAllWindowsForApplication:app
294                               withDuration:kWindowFadeAnimationDuration];
295       }
296     }
297   } while (!nextEvent);
299   // The user has released the key combo. Discard any events (i.e. the
300   // repeated KeyDown Cmd+Q).
301   [app discardEventsMatchingMask:NSAnyEventMask beforeEvent:nextEvent];
303   if (willQuit) {
304     // The user held down the combination long enough that quitting should
305     // happen.
306     confirm_quit::RecordHistogram(confirm_quit::kHoldDuration);
307     return NSTerminateNow;
308   } else {
309     // Slowly fade the confirm window out in case the user doesn't
310     // understand what they have to do to quit.
311     [self dismissPanel];
312     return NSTerminateCancel;
313   }
315   // Default case: terminate.
316   return NSTerminateNow;
319 - (void)windowWillClose:(NSNotification*)notif {
320   // Release all animations because CAAnimation retains its delegate (self),
321   // which will cause a retain cycle. Break it!
322   [[self window] setAnimations:[NSDictionary dictionary]];
323   g_confirmQuitPanelController = nil;
324   [self autorelease];
327 - (void)showWindow:(id)sender {
328   // If a panel that is fading out is going to be reused here, make sure it
329   // does not get released when the animation finishes.
330   base::scoped_nsobject<ConfirmQuitPanelController> keepAlive([self retain]);
331   [[self window] setAnimations:[NSDictionary dictionary]];
332   [[self window] center];
333   [[self window] setAlphaValue:1.0];
334   [super showWindow:sender];
337 - (void)dismissPanel {
338   [self performSelector:@selector(animateFadeOut)
339              withObject:nil
340              afterDelay:1.0];
343 - (void)animateFadeOut {
344   NSWindow* window = [self window];
345   base::scoped_nsobject<CAAnimation> animation(
346       [[window animationForKey:@"alphaValue"] copy]);
347   [animation setDelegate:self];
348   [animation setDuration:0.2];
349   NSMutableDictionary* dictionary =
350       [NSMutableDictionary dictionaryWithDictionary:[window animations]];
351   [dictionary setObject:animation forKey:@"alphaValue"];
352   [window setAnimations:dictionary];
353   [[window animator] setAlphaValue:0.0];
356 - (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)finished {
357   [self close];
360 // This looks at the Main Menu and determines what the user has set as the
361 // key combination for quit. It then gets the modifiers and builds a string
362 // to display them.
363 + (NSString*)keyCommandString {
364   scoped_ptr<ui::PlatformAcceleratorCocoa> accelerator([self quitAccelerator]);
365   return [[self class] keyCombinationForAccelerator:*accelerator];
368 // Runs a nested loop that pumps the event queue until the next KeyUp event.
369 - (NSEvent*)pumpEventQueueForKeyUp:(NSApplication*)app untilDate:(NSDate*)date {
370   return [app nextEventMatchingMask:NSKeyUpMask
371                           untilDate:date
372                              inMode:NSEventTrackingRunLoopMode
373                             dequeue:YES];
376 // Iterates through the list of open windows and hides them all.
377 - (void)hideAllWindowsForApplication:(NSApplication*)app
378                         withDuration:(NSTimeInterval)duration {
379   FadeAllWindowsAnimation* animation =
380       [[FadeAllWindowsAnimation alloc] initWithApplication:app
381                                          animationDuration:duration];
382   // Releases itself when the animation stops.
383   [animation startAnimation];
386 // This looks at the Main Menu and determines what the user has set as the
387 // key combination for quit. It then gets the modifiers and builds an object
388 // to hold the data.
389 + (scoped_ptr<ui::PlatformAcceleratorCocoa>)quitAccelerator {
390   NSMenu* mainMenu = [NSApp mainMenu];
391   // Get the application menu (i.e. Chromium).
392   NSMenu* appMenu = [[mainMenu itemAtIndex:0] submenu];
393   for (NSMenuItem* item in [appMenu itemArray]) {
394     // Find the Quit item.
395     if ([item action] == @selector(terminate:)) {
396       return scoped_ptr<ui::PlatformAcceleratorCocoa>(
397           new ui::PlatformAcceleratorCocoa([item keyEquivalent],
398                                            [item keyEquivalentModifierMask]));
399     }
400   }
401   // Default to Cmd+Q.
402   return scoped_ptr<ui::PlatformAcceleratorCocoa>(
403       new ui::PlatformAcceleratorCocoa(@"q", NSCommandKeyMask));
406 + (NSString*)keyCombinationForAccelerator:
407     (const ui::PlatformAcceleratorCocoa&)item {
408   NSMutableString* string = [NSMutableString string];
409   NSUInteger modifiers = item.modifier_mask();
411   if (modifiers & NSCommandKeyMask)
412     [string appendString:@"\u2318"];
413   if (modifiers & NSControlKeyMask)
414     [string appendString:@"\u2303"];
415   if (modifiers & NSAlternateKeyMask)
416     [string appendString:@"\u2325"];
417   if (modifiers & NSShiftKeyMask)
418     [string appendString:@"\u21E7"];
420   [string appendString:[item.characters() uppercaseString]];
421   return string;
424 @end