97375d44674fafbf071ada9ed3615e87dbf59914
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / confirm_quit_panel_controller.mm
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.
4
5 #import <Cocoa/Cocoa.h>
6 #import <QuartzCore/QuartzCore.h>
7
8 #import "chrome/browser/ui/cocoa/confirm_quit_panel_controller.h"
9
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"
24
25 // Constants ///////////////////////////////////////////////////////////////////
26
27 // How long the user must hold down Cmd+Q to confirm the quit.
28 const NSTimeInterval kTimeToConfirmQuit = 1.5;
29
30 // Leeway between the |targetDate| and the current time that will confirm a
31 // quit.
32 const NSTimeInterval kTimeDeltaFuzzFactor = 1.0;
33
34 // Duration of the window fade out animation.
35 const NSTimeInterval kWindowFadeAnimationDuration = 0.2;
36
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;
40
41 // Functions ///////////////////////////////////////////////////////////////////
42
43 namespace confirm_quit {
44
45 void RecordHistogram(ConfirmQuitMetric sample) {
46   UMA_HISTOGRAM_ENUMERATION("OSX.ConfirmToQuit", sample, kSampleCount);
47 }
48
49 void RegisterLocalState(PrefRegistrySimple* registry) {
50   registry->RegisterBooleanPref(prefs::kConfirmToQuitEnabled, false);
51 }
52
53 }  // namespace confirm_quit
54
55 // Custom Content View /////////////////////////////////////////////////////////
56
57 // The content view of the window that draws a custom frame.
58 @interface ConfirmQuitFrameView : NSView {
59  @private
60   NSTextField* message_;  // Weak, owned by the view hierarchy.
61 }
62 - (void)setMessageText:(NSString*)text;
63 @end
64
65 @implementation ConfirmQuitFrameView
66
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_];
80   }
81   return self;
82 }
83
84 - (void)drawRect:(NSRect)dirtyRect {
85   const CGFloat kCornerRadius = 5.0;
86   NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:[self bounds]
87                                                        xRadius:kCornerRadius
88                                                        yRadius:kCornerRadius];
89
90   NSColor* fillColor = [NSColor colorWithCalibratedWhite:0.2 alpha:0.75];
91   [fillColor set];
92   [path fill];
93 }
94
95 - (void)setMessageText:(NSString*)text {
96   const CGFloat kHorizontalPadding = 30;  // In view coordinates.
97
98   // Style the string.
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
103                                                                alpha:0.6]];
104   [textShadow.get() setShadowOffset:NSMakeSize(0, -1)];
105   [textShadow setShadowBlurRadius:1.0];
106   [attrString addAttribute:NSShadowAttributeName
107                      value:textShadow
108                      range:NSMakeRange(0, [text length])];
109   [message_ setAttributedStringValue:attrString];
110
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];
116
117   if (NSWidth(messageFrame) > NSWidth(frameInViewSpace))
118     frameInViewSpace.size.width = NSWidth(messageFrame) + kHorizontalPadding;
119
120   messageFrame.origin.x = NSWidth(frameInViewSpace) / 2 - NSMidX(messageFrame);
121   messageFrame.origin.y = NSHeight(frameInViewSpace) / 2 - NSMidY(messageFrame);
122
123   [[self window] setFrame:[message_ convertRect:frameInViewSpace toView:nil]
124                   display:YES];
125   [message_ setFrame:messageFrame];
126 }
127
128 @end
129
130 // Animation ///////////////////////////////////////////////////////////////////
131
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> {
136  @private
137   NSApplication* application_;
138 }
139 - (id)initWithApplication:(NSApplication*)app
140         animationDuration:(NSTimeInterval)duration;
141 @end
142
143
144 @implementation FadeAllWindowsAnimation
145
146 - (id)initWithApplication:(NSApplication*)app
147         animationDuration:(NSTimeInterval)duration {
148   if ((self = [super initWithDuration:duration
149                        animationCurve:NSAnimationLinear])) {
150     application_ = app;
151     [self setDelegate:self];
152   }
153   return self;
154 }
155
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];
161     }
162   }
163 }
164
165 - (void)animationDidStop:(NSAnimation*)anim {
166   DCHECK_EQ(self, anim);
167   [self autorelease];
168 }
169
170 @end
171
172 // Private Interface ///////////////////////////////////////////////////////////
173
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;
181 @end
182
183 ConfirmQuitPanelController* g_confirmQuitPanelController = nil;
184
185 ////////////////////////////////////////////////////////////////////////////////
186
187 @implementation ConfirmQuitPanelController
188
189 + (ConfirmQuitPanelController*)sharedController {
190   if (!g_confirmQuitPanelController) {
191     g_confirmQuitPanelController =
192         [[ConfirmQuitPanelController alloc] init];
193   }
194   return [[g_confirmQuitPanelController retain] autorelease];
195 }
196
197 - (id)init {
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
203                                       defer:NO]);
204   if ((self = [super initWithWindow:window])) {
205     [window setDelegate:self];
206     [window setBackgroundColor:[NSColor clearColor]];
207     [window setOpaque:NO];
208     [window setHasShadow:NO];
209
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_];
216
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];
221   }
222   return self;
223 }
224
225 + (BOOL)eventTriggersFeature:(NSEvent*)event {
226   if ([event type] != NSKeyDown)
227     return NO;
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);
234 }
235
236 - (NSApplicationTerminateReply)runModalLoopForApplication:(NSApplication*)app {
237   base::scoped_nsobject<ConfirmQuitPanelController> keepAlive([self retain]);
238
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];
258
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);
262     else
263       confirm_quit::RecordHistogram(confirm_quit::kTapHold);
264     return NSTerminateNow;
265   } else {
266     [lastQuitAttempt release];  // Harmless if already nil.
267     lastQuitAttempt = [timeNow retain];  // Record this attempt for next time.
268   }
269
270   // Show the info panel that explains what the user must to do confirm quit.
271   [self showWindow:self];
272
273   // Spin a nested run loop until the |targetDate| is reached or a KeyUp event
274   // is sent.
275   NSDate* targetDate = [NSDate dateWithTimeIntervalSinceNow:kTimeToConfirmQuit];
276   BOOL willQuit = NO;
277   NSEvent* nextEvent = nil;
278   do {
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
281     // action.
282     NSDate* waitDate = [NSDate dateWithTimeIntervalSinceNow:
283         kTimeToConfirmQuit - kTimeDeltaFuzzFactor];
284     nextEvent = [self pumpEventQueueForKeyUp:app untilDate:waitDate];
285
286     // Wait for the time expiry to happen. Once past the hold threshold,
287     // commit to quitting and hide all the open windows.
288     if (!willQuit) {
289       NSDate* now = [NSDate date];
290       NSTimeInterval difference = [targetDate timeIntervalSinceDate:now];
291       if (difference < kTimeDeltaFuzzFactor) {
292         willQuit = YES;
293
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
296         // the quit.
297         [self hideAllWindowsForApplication:app
298                               withDuration:kWindowFadeAnimationDuration];
299       }
300     }
301   } while (!nextEvent);
302
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];
306
307   if (willQuit) {
308     // The user held down the combination long enough that quitting should
309     // happen.
310     confirm_quit::RecordHistogram(confirm_quit::kHoldDuration);
311     return NSTerminateNow;
312   } else {
313     // Slowly fade the confirm window out in case the user doesn't
314     // understand what they have to do to quit.
315     [self dismissPanel];
316     return NSTerminateCancel;
317   }
318
319   // Default case: terminate.
320   return NSTerminateNow;
321 }
322
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;
328   [self autorelease];
329 }
330
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];
339 }
340
341 - (void)dismissPanel {
342   [self performSelector:@selector(animateFadeOut)
343              withObject:nil
344              afterDelay:1.0];
345 }
346
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];
358 }
359
360 - (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)finished {
361   [self close];
362 }
363
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
366 // to display them.
367 + (NSString*)keyCommandString {
368   scoped_ptr<ui::PlatformAcceleratorCocoa> accelerator([self quitAccelerator]);
369   return [[self class] keyCombinationForAccelerator:*accelerator];
370 }
371
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
375                           untilDate:date
376                              inMode:NSEventTrackingRunLoopMode
377                             dequeue:YES];
378 }
379
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];
388 }
389
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
392 // to hold the data.
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]));
403     }
404   }
405   // Default to Cmd+Q.
406   return scoped_ptr<ui::PlatformAcceleratorCocoa>(
407       new ui::PlatformAcceleratorCocoa(@"q", NSCommandKeyMask));
408 }
409
410 + (NSString*)keyCombinationForAccelerator:
411     (const ui::PlatformAcceleratorCocoa&)item {
412   NSMutableString* string = [NSMutableString string];
413   NSUInteger modifiers = item.modifier_mask();
414
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"];
423
424   [string appendString:[item.characters() uppercaseString]];
425   return string;
426 }
427
428 @end