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
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 {
59 NSTextField* message_; // Weak, owned by the view hierarchy.
61 - (void)setMessageText:(NSString*)text;
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_];
83 - (void)drawRect:(NSRect)dirtyRect {
84 const CGFloat kCornerRadius = 5.0;
85 NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:[self bounds]
87 yRadius:kCornerRadius];
89 NSColor* fillColor = [NSColor colorWithCalibratedWhite:0.2 alpha:0.75];
94 - (void)setMessageText:(NSString*)text {
95 const CGFloat kHorizontalPadding = 30; // In view coordinates.
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
103 [textShadow.get() setShadowOffset:NSMakeSize(0, -1)];
104 [textShadow setShadowBlurRadius:1.0];
105 [attrString addAttribute:NSShadowAttributeName
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]
124 [message_ setFrame:messageFrame];
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> {
136 NSApplication* application_;
138 - (id)initWithApplication:(NSApplication*)app
139 animationDuration:(NSTimeInterval)duration;
143 @implementation FadeAllWindowsAnimation
145 - (id)initWithApplication:(NSApplication*)app
146 animationDuration:(NSTimeInterval)duration {
147 if ((self = [super initWithDuration:duration
148 animationCurve:NSAnimationLinear])) {
150 [self setDelegate:self];
155 - (void)setCurrentProgress:(NSAnimationProgress)progress {
156 for (NSWindow* window in [application_ windows]) {
157 [window setAlphaValue:1.0 - progress];
161 - (void)animationDidStop:(NSAnimation*)anim {
162 DCHECK_EQ(self, anim);
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;
179 ConfirmQuitPanelController* g_confirmQuitPanelController = nil;
181 ////////////////////////////////////////////////////////////////////////////////
183 @implementation ConfirmQuitPanelController
185 + (ConfirmQuitPanelController*)sharedController {
186 if (!g_confirmQuitPanelController) {
187 g_confirmQuitPanelController =
188 [[ConfirmQuitPanelController alloc] init];
190 return [[g_confirmQuitPanelController retain] autorelease];
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
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];
221 + (BOOL)eventTriggersFeature:(NSEvent*)event {
222 if ([event type] != NSKeyDown)
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);
259 confirm_quit::RecordHistogram(confirm_quit::kTapHold);
260 return NSTerminateNow;
262 [lastQuitAttempt release]; // Harmless if already nil.
263 lastQuitAttempt = [timeNow retain]; // Record this attempt for next time.
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
271 NSDate* targetDate = [NSDate dateWithTimeIntervalSinceNow:kTimeToConfirmQuit];
273 NSEvent* nextEvent = nil;
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
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.
285 NSDate* now = [NSDate date];
286 NSTimeInterval difference = [targetDate timeIntervalSinceDate:now];
287 if (difference < kTimeDeltaFuzzFactor) {
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
293 [self hideAllWindowsForApplication:app
294 withDuration:kWindowFadeAnimationDuration];
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];
304 // The user held down the combination long enough that quitting should
306 confirm_quit::RecordHistogram(confirm_quit::kHoldDuration);
307 return NSTerminateNow;
309 // Slowly fade the confirm window out in case the user doesn't
310 // understand what they have to do to quit.
312 return NSTerminateCancel;
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;
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)
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 {
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
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
372 inMode:NSEventTrackingRunLoopMode
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
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]));
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]];