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 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
19 #include "chrome/browser/ui/cocoa/confirm_quit.h"
20 #include "chrome/common/pref_names.h"
21 #include "chrome/grit/generated_resources.h"
22 #import "ui/base/accelerators/platform_accelerator_cocoa.h"
23 #include "ui/base/l10n/l10n_util_mac.h"
25 // Constants ///////////////////////////////////////////////////////////////////
27 // How long the user must hold down Cmd+Q to confirm the quit.
28 const NSTimeInterval kTimeToConfirmQuit = 1.5;
30 // Leeway between the |targetDate| and the current time that will confirm a
32 const NSTimeInterval kTimeDeltaFuzzFactor = 1.0;
34 // Duration of the window fade out animation.
35 const NSTimeInterval kWindowFadeAnimationDuration = 0.2;
37 // For metrics recording only: How long the user must hold the keys to
38 // differentitate kDoubleTap from kTapHold.
39 const NSTimeInterval kDoubleTapTimeDelta = 0.32;
41 // Functions ///////////////////////////////////////////////////////////////////
43 namespace confirm_quit {
45 void RecordHistogram(ConfirmQuitMetric sample) {
46 UMA_HISTOGRAM_ENUMERATION("OSX.ConfirmToQuit", sample, kSampleCount);
49 void RegisterLocalState(PrefRegistrySimple* registry) {
50 registry->RegisterBooleanPref(prefs::kConfirmToQuitEnabled, false);
53 } // namespace confirm_quit
55 // Custom Content View /////////////////////////////////////////////////////////
57 // The content view of the window that draws a custom frame.
58 @interface ConfirmQuitFrameView : NSView {
60 NSTextField* message_; // Weak, owned by the view hierarchy.
62 - (void)setMessageText:(NSString*)text;
65 @implementation ConfirmQuitFrameView
67 - (id)initWithFrame:(NSRect)frameRect {
68 if ((self = [super initWithFrame:frameRect])) {
69 base::scoped_nsobject<NSTextField> message(
70 // The frame will be fixed up when |-setMessageText:| is called.
71 [[NSTextField alloc] initWithFrame:NSZeroRect]);
72 message_ = message.get();
73 [message_ setEditable:NO];
74 [message_ setSelectable:NO];
75 [message_ setBezeled:NO];
76 [message_ setDrawsBackground:NO];
77 [message_ setFont:[NSFont boldSystemFontOfSize:24]];
78 [message_ setTextColor:[NSColor whiteColor]];
79 [self addSubview:message_];
84 - (void)drawRect:(NSRect)dirtyRect {
85 const CGFloat kCornerRadius = 5.0;
86 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:[self bounds]
88 yRadius:kCornerRadius];
90 NSColor* fillColor = [NSColor colorWithCalibratedWhite:0.2 alpha:0.75];
95 - (void)setMessageText:(NSString*)text {
96 const CGFloat kHorizontalPadding = 30; // In view coordinates.
99 base::scoped_nsobject<NSMutableAttributedString> attrString(
100 [[NSMutableAttributedString alloc] initWithString:text]);
101 base::scoped_nsobject<NSShadow> textShadow([[NSShadow alloc] init]);
102 [textShadow.get() setShadowColor:[NSColor colorWithCalibratedWhite:0
104 [textShadow.get() setShadowOffset:NSMakeSize(0, -1)];
105 [textShadow setShadowBlurRadius:1.0];
106 [attrString addAttribute:NSShadowAttributeName
108 range:NSMakeRange(0, [text length])];
109 [message_ setAttributedStringValue:attrString];
111 // Fixup the frame of the string.
112 [message_ sizeToFit];
113 NSRect messageFrame = [message_ frame];
114 NSRect frameInViewSpace =
115 [message_ convertRect:[[self window] frame] fromView:nil];
117 if (NSWidth(messageFrame) > NSWidth(frameInViewSpace))
118 frameInViewSpace.size.width = NSWidth(messageFrame) + kHorizontalPadding;
120 messageFrame.origin.x = NSWidth(frameInViewSpace) / 2 - NSMidX(messageFrame);
121 messageFrame.origin.y = NSHeight(frameInViewSpace) / 2 - NSMidY(messageFrame);
123 [[self window] setFrame:[message_ convertRect:frameInViewSpace toView:nil]
125 [message_ setFrame:messageFrame];
130 // Animation ///////////////////////////////////////////////////////////////////
132 // This animation will run through all the windows of the passed-in
133 // NSApplication and will fade their alpha value to 0.0. When the animation is
134 // complete, this will release itself.
135 @interface FadeAllWindowsAnimation : NSAnimation<NSAnimationDelegate> {
137 NSApplication* application_;
139 - (id)initWithApplication:(NSApplication*)app
140 animationDuration:(NSTimeInterval)duration;
144 @implementation FadeAllWindowsAnimation
146 - (id)initWithApplication:(NSApplication*)app
147 animationDuration:(NSTimeInterval)duration {
148 if ((self = [super initWithDuration:duration
149 animationCurve:NSAnimationLinear])) {
151 [self setDelegate:self];
156 - (void)setCurrentProgress:(NSAnimationProgress)progress {
157 for (NSWindow* window in [application_ windows]) {
158 if ([[window windowController]
159 isKindOfClass:[BrowserWindowController class]]) {
160 [window setAlphaValue:1.0 - progress];
165 - (void)animationDidStop:(NSAnimation*)anim {
166 DCHECK_EQ(self, anim);
172 // Private Interface ///////////////////////////////////////////////////////////
174 @interface ConfirmQuitPanelController (Private)
175 - (void)animateFadeOut;
176 - (NSEvent*)pumpEventQueueForKeyUp:(NSApplication*)app untilDate:(NSDate*)date;
177 - (void)hideAllWindowsForApplication:(NSApplication*)app
178 withDuration:(NSTimeInterval)duration;
179 // Returns the Accelerator for the Quit menu item.
180 + (scoped_ptr<ui::PlatformAcceleratorCocoa>)quitAccelerator;
183 ConfirmQuitPanelController* g_confirmQuitPanelController = nil;
185 ////////////////////////////////////////////////////////////////////////////////
187 @implementation ConfirmQuitPanelController
189 + (ConfirmQuitPanelController*)sharedController {
190 if (!g_confirmQuitPanelController) {
191 g_confirmQuitPanelController =
192 [[ConfirmQuitPanelController alloc] init];
194 return [[g_confirmQuitPanelController retain] autorelease];
198 const NSRect kWindowFrame = NSMakeRect(0, 0, 350, 70);
199 base::scoped_nsobject<NSWindow> window(
200 [[NSWindow alloc] initWithContentRect:kWindowFrame
201 styleMask:NSBorderlessWindowMask
202 backing:NSBackingStoreBuffered
204 if ((self = [super initWithWindow:window])) {
205 [window setDelegate:self];
206 [window setBackgroundColor:[NSColor clearColor]];
207 [window setOpaque:NO];
208 [window setHasShadow:NO];
210 // Create the content view. Take the frame from the existing content view.
211 NSRect frame = [[window contentView] frame];
212 base::scoped_nsobject<ConfirmQuitFrameView> frameView(
213 [[ConfirmQuitFrameView alloc] initWithFrame:frame]);
214 contentView_ = frameView.get();
215 [window setContentView:contentView_];
217 // Set the proper string.
218 NSString* message = l10n_util::GetNSStringF(IDS_CONFIRM_TO_QUIT_DESCRIPTION,
219 base::SysNSStringToUTF16([[self class] keyCommandString]));
220 [contentView_ setMessageText:message];
225 + (BOOL)eventTriggersFeature:(NSEvent*)event {
226 if ([event type] != NSKeyDown)
228 ui::PlatformAcceleratorCocoa eventAccelerator(
229 [event charactersIgnoringModifiers],
230 [event modifierFlags] & NSDeviceIndependentModifierFlagsMask);
231 scoped_ptr<ui::PlatformAcceleratorCocoa> quitAccelerator(
232 [self quitAccelerator]);
233 return quitAccelerator->Equals(eventAccelerator);
236 - (NSApplicationTerminateReply)runModalLoopForApplication:(NSApplication*)app {
237 base::scoped_nsobject<ConfirmQuitPanelController> keepAlive([self retain]);
239 // If this is the second of two such attempts to quit within a certain time
240 // interval, then just quit.
241 // Time of last quit attempt, if any.
242 static NSDate* lastQuitAttempt; // Initially nil, as it's static.
243 NSDate* timeNow = [NSDate date];
244 if (lastQuitAttempt &&
245 [timeNow timeIntervalSinceDate:lastQuitAttempt] < kTimeDeltaFuzzFactor) {
246 // The panel tells users to Hold Cmd+Q. However, we also want to have a
247 // double-tap shortcut that allows for a quick quit path. For the users who
248 // tap Cmd+Q and then hold it with the window still open, this double-tap
249 // logic will run and cause the quit to get committed. If the key
250 // combination held down, the system will start sending the Cmd+Q event to
251 // the next key application, and so on. This is bad, so instead we hide all
252 // the windows (without animation) to look like we've "quit" and then wait
253 // for the KeyUp event to commit the quit.
254 [self hideAllWindowsForApplication:app withDuration:0];
255 NSEvent* nextEvent = [self pumpEventQueueForKeyUp:app
256 untilDate:[NSDate distantFuture]];
257 [app discardEventsMatchingMask:NSAnyEventMask beforeEvent:nextEvent];
259 // Based on how long the user held the keys, record the metric.
260 if ([[NSDate date] timeIntervalSinceDate:timeNow] < kDoubleTapTimeDelta)
261 confirm_quit::RecordHistogram(confirm_quit::kDoubleTap);
263 confirm_quit::RecordHistogram(confirm_quit::kTapHold);
264 return NSTerminateNow;
266 [lastQuitAttempt release]; // Harmless if already nil.
267 lastQuitAttempt = [timeNow retain]; // Record this attempt for next time.
270 // Show the info panel that explains what the user must to do confirm quit.
271 [self showWindow:self];
273 // Spin a nested run loop until the |targetDate| is reached or a KeyUp event
275 NSDate* targetDate = [NSDate dateWithTimeIntervalSinceNow:kTimeToConfirmQuit];
277 NSEvent* nextEvent = nil;
279 // Dequeue events until a key up is received. To avoid busy waiting, figure
280 // out the amount of time that the thread can sleep before taking further
282 NSDate* waitDate = [NSDate dateWithTimeIntervalSinceNow:
283 kTimeToConfirmQuit - kTimeDeltaFuzzFactor];
284 nextEvent = [self pumpEventQueueForKeyUp:app untilDate:waitDate];
286 // Wait for the time expiry to happen. Once past the hold threshold,
287 // commit to quitting and hide all the open windows.
289 NSDate* now = [NSDate date];
290 NSTimeInterval difference = [targetDate timeIntervalSinceDate:now];
291 if (difference < kTimeDeltaFuzzFactor) {
294 // At this point, the quit has been confirmed and windows should all
295 // fade out to convince the user to release the key combo to finalize
297 [self hideAllWindowsForApplication:app
298 withDuration:kWindowFadeAnimationDuration];
301 } while (!nextEvent);
303 // The user has released the key combo. Discard any events (i.e. the
304 // repeated KeyDown Cmd+Q).
305 [app discardEventsMatchingMask:NSAnyEventMask beforeEvent:nextEvent];
308 // The user held down the combination long enough that quitting should
310 confirm_quit::RecordHistogram(confirm_quit::kHoldDuration);
311 return NSTerminateNow;
313 // Slowly fade the confirm window out in case the user doesn't
314 // understand what they have to do to quit.
316 return NSTerminateCancel;
319 // Default case: terminate.
320 return NSTerminateNow;
323 - (void)windowWillClose:(NSNotification*)notif {
324 // Release all animations because CAAnimation retains its delegate (self),
325 // which will cause a retain cycle. Break it!
326 [[self window] setAnimations:[NSDictionary dictionary]];
327 g_confirmQuitPanelController = nil;
331 - (void)showWindow:(id)sender {
332 // If a panel that is fading out is going to be reused here, make sure it
333 // does not get released when the animation finishes.
334 base::scoped_nsobject<ConfirmQuitPanelController> keepAlive([self retain]);
335 [[self window] setAnimations:[NSDictionary dictionary]];
336 [[self window] center];
337 [[self window] setAlphaValue:1.0];
338 [super showWindow:sender];
341 - (void)dismissPanel {
342 [self performSelector:@selector(animateFadeOut)
347 - (void)animateFadeOut {
348 NSWindow* window = [self window];
349 base::scoped_nsobject<CAAnimation> animation(
350 [[window animationForKey:@"alphaValue"] copy]);
351 [animation setDelegate:self];
352 [animation setDuration:0.2];
353 NSMutableDictionary* dictionary =
354 [NSMutableDictionary dictionaryWithDictionary:[window animations]];
355 [dictionary setObject:animation forKey:@"alphaValue"];
356 [window setAnimations:dictionary];
357 [[window animator] setAlphaValue:0.0];
360 - (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)finished {
364 // This looks at the Main Menu and determines what the user has set as the
365 // key combination for quit. It then gets the modifiers and builds a string
367 + (NSString*)keyCommandString {
368 scoped_ptr<ui::PlatformAcceleratorCocoa> accelerator([self quitAccelerator]);
369 return [[self class] keyCombinationForAccelerator:*accelerator];
372 // Runs a nested loop that pumps the event queue until the next KeyUp event.
373 - (NSEvent*)pumpEventQueueForKeyUp:(NSApplication*)app untilDate:(NSDate*)date {
374 return [app nextEventMatchingMask:NSKeyUpMask
376 inMode:NSEventTrackingRunLoopMode
380 // Iterates through the list of open windows and hides them all.
381 - (void)hideAllWindowsForApplication:(NSApplication*)app
382 withDuration:(NSTimeInterval)duration {
383 FadeAllWindowsAnimation* animation =
384 [[FadeAllWindowsAnimation alloc] initWithApplication:app
385 animationDuration:duration];
386 // Releases itself when the animation stops.
387 [animation startAnimation];
390 // This looks at the Main Menu and determines what the user has set as the
391 // key combination for quit. It then gets the modifiers and builds an object
393 + (scoped_ptr<ui::PlatformAcceleratorCocoa>)quitAccelerator {
394 NSMenu* mainMenu = [NSApp mainMenu];
395 // Get the application menu (i.e. Chromium).
396 NSMenu* appMenu = [[mainMenu itemAtIndex:0] submenu];
397 for (NSMenuItem* item in [appMenu itemArray]) {
398 // Find the Quit item.
399 if ([item action] == @selector(terminate:)) {
400 return scoped_ptr<ui::PlatformAcceleratorCocoa>(
401 new ui::PlatformAcceleratorCocoa([item keyEquivalent],
402 [item keyEquivalentModifierMask]));
406 return scoped_ptr<ui::PlatformAcceleratorCocoa>(
407 new ui::PlatformAcceleratorCocoa(@"q", NSCommandKeyMask));
410 + (NSString*)keyCombinationForAccelerator:
411 (const ui::PlatformAcceleratorCocoa&)item {
412 NSMutableString* string = [NSMutableString string];
413 NSUInteger modifiers = item.modifier_mask();
415 if (modifiers & NSCommandKeyMask)
416 [string appendString:@"\u2318"];
417 if (modifiers & NSControlKeyMask)
418 [string appendString:@"\u2303"];
419 if (modifiers & NSAlternateKeyMask)
420 [string appendString:@"\u2325"];
421 if (modifiers & NSShiftKeyMask)
422 [string appendString:@"\u21E7"];
424 [string appendString:[item.characters() uppercaseString]];