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/browser_finder.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 (chrome::FindBrowserWithWindow(window))
159 [window setAlphaValue:1.0 - progress];
163 - (void)animationDidStop:(NSAnimation*)anim {
164 DCHECK_EQ(self, anim);
170 // Private Interface ///////////////////////////////////////////////////////////
172 @interface ConfirmQuitPanelController (Private)
173 - (void)animateFadeOut;
174 - (NSEvent*)pumpEventQueueForKeyUp:(NSApplication*)app untilDate:(NSDate*)date;
175 - (void)hideAllWindowsForApplication:(NSApplication*)app
176 withDuration:(NSTimeInterval)duration;
177 // Returns the Accelerator for the Quit menu item.
178 + (scoped_ptr<ui::PlatformAcceleratorCocoa>)quitAccelerator;
181 ConfirmQuitPanelController* g_confirmQuitPanelController = nil;
183 ////////////////////////////////////////////////////////////////////////////////
185 @implementation ConfirmQuitPanelController
187 + (ConfirmQuitPanelController*)sharedController {
188 if (!g_confirmQuitPanelController) {
189 g_confirmQuitPanelController =
190 [[ConfirmQuitPanelController alloc] init];
192 return [[g_confirmQuitPanelController retain] autorelease];
196 const NSRect kWindowFrame = NSMakeRect(0, 0, 350, 70);
197 base::scoped_nsobject<NSWindow> window(
198 [[NSWindow alloc] initWithContentRect:kWindowFrame
199 styleMask:NSBorderlessWindowMask
200 backing:NSBackingStoreBuffered
202 if ((self = [super initWithWindow:window])) {
203 [window setDelegate:self];
204 [window setBackgroundColor:[NSColor clearColor]];
205 [window setOpaque:NO];
206 [window setHasShadow:NO];
208 // Create the content view. Take the frame from the existing content view.
209 NSRect frame = [[window contentView] frame];
210 base::scoped_nsobject<ConfirmQuitFrameView> frameView(
211 [[ConfirmQuitFrameView alloc] initWithFrame:frame]);
212 contentView_ = frameView.get();
213 [window setContentView:contentView_];
215 // Set the proper string.
216 NSString* message = l10n_util::GetNSStringF(IDS_CONFIRM_TO_QUIT_DESCRIPTION,
217 base::SysNSStringToUTF16([[self class] keyCommandString]));
218 [contentView_ setMessageText:message];
223 + (BOOL)eventTriggersFeature:(NSEvent*)event {
224 if ([event type] != NSKeyDown)
226 ui::PlatformAcceleratorCocoa eventAccelerator(
227 [event charactersIgnoringModifiers],
228 [event modifierFlags] & NSDeviceIndependentModifierFlagsMask);
229 scoped_ptr<ui::PlatformAcceleratorCocoa> quitAccelerator(
230 [self quitAccelerator]);
231 return quitAccelerator->Equals(eventAccelerator);
234 - (NSApplicationTerminateReply)runModalLoopForApplication:(NSApplication*)app {
235 base::scoped_nsobject<ConfirmQuitPanelController> keepAlive([self retain]);
237 // If this is the second of two such attempts to quit within a certain time
238 // interval, then just quit.
239 // Time of last quit attempt, if any.
240 static NSDate* lastQuitAttempt; // Initially nil, as it's static.
241 NSDate* timeNow = [NSDate date];
242 if (lastQuitAttempt &&
243 [timeNow timeIntervalSinceDate:lastQuitAttempt] < kTimeDeltaFuzzFactor) {
244 // The panel tells users to Hold Cmd+Q. However, we also want to have a
245 // double-tap shortcut that allows for a quick quit path. For the users who
246 // tap Cmd+Q and then hold it with the window still open, this double-tap
247 // logic will run and cause the quit to get committed. If the key
248 // combination held down, the system will start sending the Cmd+Q event to
249 // the next key application, and so on. This is bad, so instead we hide all
250 // the windows (without animation) to look like we've "quit" and then wait
251 // for the KeyUp event to commit the quit.
252 [self hideAllWindowsForApplication:app withDuration:0];
253 NSEvent* nextEvent = [self pumpEventQueueForKeyUp:app
254 untilDate:[NSDate distantFuture]];
255 [app discardEventsMatchingMask:NSAnyEventMask beforeEvent:nextEvent];
257 // Based on how long the user held the keys, record the metric.
258 if ([[NSDate date] timeIntervalSinceDate:timeNow] < kDoubleTapTimeDelta)
259 confirm_quit::RecordHistogram(confirm_quit::kDoubleTap);
261 confirm_quit::RecordHistogram(confirm_quit::kTapHold);
262 return NSTerminateNow;
264 [lastQuitAttempt release]; // Harmless if already nil.
265 lastQuitAttempt = [timeNow retain]; // Record this attempt for next time.
268 // Show the info panel that explains what the user must to do confirm quit.
269 [self showWindow:self];
271 // Spin a nested run loop until the |targetDate| is reached or a KeyUp event
273 NSDate* targetDate = [NSDate dateWithTimeIntervalSinceNow:kTimeToConfirmQuit];
275 NSEvent* nextEvent = nil;
277 // Dequeue events until a key up is received. To avoid busy waiting, figure
278 // out the amount of time that the thread can sleep before taking further
280 NSDate* waitDate = [NSDate dateWithTimeIntervalSinceNow:
281 kTimeToConfirmQuit - kTimeDeltaFuzzFactor];
282 nextEvent = [self pumpEventQueueForKeyUp:app untilDate:waitDate];
284 // Wait for the time expiry to happen. Once past the hold threshold,
285 // commit to quitting and hide all the open windows.
287 NSDate* now = [NSDate date];
288 NSTimeInterval difference = [targetDate timeIntervalSinceDate:now];
289 if (difference < kTimeDeltaFuzzFactor) {
292 // At this point, the quit has been confirmed and windows should all
293 // fade out to convince the user to release the key combo to finalize
295 [self hideAllWindowsForApplication:app
296 withDuration:kWindowFadeAnimationDuration];
299 } while (!nextEvent);
301 // The user has released the key combo. Discard any events (i.e. the
302 // repeated KeyDown Cmd+Q).
303 [app discardEventsMatchingMask:NSAnyEventMask beforeEvent:nextEvent];
306 // The user held down the combination long enough that quitting should
308 confirm_quit::RecordHistogram(confirm_quit::kHoldDuration);
309 return NSTerminateNow;
311 // Slowly fade the confirm window out in case the user doesn't
312 // understand what they have to do to quit.
314 return NSTerminateCancel;
317 // Default case: terminate.
318 return NSTerminateNow;
321 - (void)windowWillClose:(NSNotification*)notif {
322 // Release all animations because CAAnimation retains its delegate (self),
323 // which will cause a retain cycle. Break it!
324 [[self window] setAnimations:[NSDictionary dictionary]];
325 g_confirmQuitPanelController = nil;
329 - (void)showWindow:(id)sender {
330 // If a panel that is fading out is going to be reused here, make sure it
331 // does not get released when the animation finishes.
332 base::scoped_nsobject<ConfirmQuitPanelController> keepAlive([self retain]);
333 [[self window] setAnimations:[NSDictionary dictionary]];
334 [[self window] center];
335 [[self window] setAlphaValue:1.0];
336 [super showWindow:sender];
339 - (void)dismissPanel {
340 [self performSelector:@selector(animateFadeOut)
345 - (void)animateFadeOut {
346 NSWindow* window = [self window];
347 base::scoped_nsobject<CAAnimation> animation(
348 [[window animationForKey:@"alphaValue"] copy]);
349 [animation setDelegate:self];
350 [animation setDuration:0.2];
351 NSMutableDictionary* dictionary =
352 [NSMutableDictionary dictionaryWithDictionary:[window animations]];
353 [dictionary setObject:animation forKey:@"alphaValue"];
354 [window setAnimations:dictionary];
355 [[window animator] setAlphaValue:0.0];
358 - (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)finished {
362 // This looks at the Main Menu and determines what the user has set as the
363 // key combination for quit. It then gets the modifiers and builds a string
365 + (NSString*)keyCommandString {
366 scoped_ptr<ui::PlatformAcceleratorCocoa> accelerator([self quitAccelerator]);
367 return [[self class] keyCombinationForAccelerator:*accelerator];
370 // Runs a nested loop that pumps the event queue until the next KeyUp event.
371 - (NSEvent*)pumpEventQueueForKeyUp:(NSApplication*)app untilDate:(NSDate*)date {
372 return [app nextEventMatchingMask:NSKeyUpMask
374 inMode:NSEventTrackingRunLoopMode
378 // Iterates through the list of open windows and hides them all.
379 - (void)hideAllWindowsForApplication:(NSApplication*)app
380 withDuration:(NSTimeInterval)duration {
381 FadeAllWindowsAnimation* animation =
382 [[FadeAllWindowsAnimation alloc] initWithApplication:app
383 animationDuration:duration];
384 // Releases itself when the animation stops.
385 [animation startAnimation];
388 // This looks at the Main Menu and determines what the user has set as the
389 // key combination for quit. It then gets the modifiers and builds an object
391 + (scoped_ptr<ui::PlatformAcceleratorCocoa>)quitAccelerator {
392 NSMenu* mainMenu = [NSApp mainMenu];
393 // Get the application menu (i.e. Chromium).
394 NSMenu* appMenu = [[mainMenu itemAtIndex:0] submenu];
395 for (NSMenuItem* item in [appMenu itemArray]) {
396 // Find the Quit item.
397 if ([item action] == @selector(terminate:)) {
398 return scoped_ptr<ui::PlatformAcceleratorCocoa>(
399 new ui::PlatformAcceleratorCocoa([item keyEquivalent],
400 [item keyEquivalentModifierMask]));
404 return scoped_ptr<ui::PlatformAcceleratorCocoa>(
405 new ui::PlatformAcceleratorCocoa(@"q", NSCommandKeyMask));
408 + (NSString*)keyCombinationForAccelerator:
409 (const ui::PlatformAcceleratorCocoa&)item {
410 NSMutableString* string = [NSMutableString string];
411 NSUInteger modifiers = item.modifier_mask();
413 if (modifiers & NSCommandKeyMask)
414 [string appendString:@"\u2318"];
415 if (modifiers & NSControlKeyMask)
416 [string appendString:@"\u2303"];
417 if (modifiers & NSAlternateKeyMask)
418 [string appendString:@"\u2325"];
419 if (modifiers & NSShiftKeyMask)
420 [string appendString:@"\u21E7"];
422 [string appendString:[item.characters() uppercaseString]];