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
7#include "base/logging.h"  // for NOTREACHED()
8#include "base/mac/bundle_locations.h"
9#include "base/mac/mac_util.h"
10#include "base/strings/sys_string_conversions.h"
11#include "base/strings/utf_string_conversions.h"
12#include "chrome/app/chrome_command_ids.h"
13#include "chrome/browser/profiles/profile.h"
14#include "chrome/browser/ui/browser.h"
15#include "chrome/browser/ui/browser_commands.h"
16#import "chrome/browser/ui/cocoa/browser_window_controller.h"
17#import "chrome/browser/ui/cocoa/fullscreen_exit_bubble_controller.h"
18#import "chrome/browser/ui/cocoa/info_bubble_view.h"
19#import "chrome/browser/ui/cocoa/info_bubble_window.h"
20#include "chrome/browser/ui/fullscreen/fullscreen_controller.h"
21#include "chrome/browser/ui/fullscreen/fullscreen_exit_bubble_type.h"
22#include "chrome/grit/generated_resources.h"
23#include "extensions/browser/extension_system.h"
24#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
25#include "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
26#import "third_party/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
27#include "ui/base/accelerators/platform_accelerator_cocoa.h"
28#import "ui/base/cocoa/controls/hyperlink_text_view.h"
29#include "ui/base/l10n/l10n_util.h"
30#include "ui/base/l10n/l10n_util_mac.h"
31#include "ui/strings/grit/ui_strings.h"
32
33
34namespace {
35const float kInitialDelay = 3.8;
36const float kHideDuration = 0.7;
37} // namespace
38
39@interface OneClickHyperlinkTextView : HyperlinkTextView
40@end
41@implementation OneClickHyperlinkTextView
42- (BOOL)acceptsFirstMouse:(NSEvent*)event {
43  return YES;
44}
45@end
46
47@interface FullscreenExitBubbleController (PrivateMethods)
48// Sets |exitLabel_| based on |exitLabelPlaceholder_|,
49// sets |exitLabelPlaceholder_| to nil.
50- (void)initializeLabel;
51
52- (NSString*)getLabelText;
53
54- (void)hideSoon;
55
56// Returns the Accelerator for the Toggle Fullscreen menu item.
57+ (scoped_ptr<ui::PlatformAcceleratorCocoa>)acceleratorForToggleFullscreen;
58
59// Returns a string representation fit for display of
60// +acceleratorForToggleFullscreen.
61+ (NSString*)keyCommandString;
62
63+ (NSString*)keyCombinationForAccelerator:
64    (const ui::PlatformAcceleratorCocoa&)item;
65@end
66
67@implementation FullscreenExitBubbleController
68
69- (id)initWithOwner:(BrowserWindowController*)owner
70            browser:(Browser*)browser
71                url:(const GURL&)url
72         bubbleType:(FullscreenExitBubbleType)bubbleType {
73  NSString* nibPath =
74      [base::mac::FrameworkBundle() pathForResource:@"FullscreenExitBubble"
75                                             ofType:@"nib"];
76  if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
77    browser_ = browser;
78    owner_ = owner;
79    url_ = url;
80    bubbleType_ = bubbleType;
81    // Mouse lock expects mouse events to reach the main window immediately.
82    // Make the bubble transparent for mouse events if mouse lock is enabled.
83    if (bubbleType_ == FEB_TYPE_FULLSCREEN_MOUSELOCK_EXIT_INSTRUCTION ||
84        bubbleType_ == FEB_TYPE_MOUSELOCK_EXIT_INSTRUCTION)
85      [[self window] setIgnoresMouseEvents:YES];
86  }
87  return self;
88}
89
90- (void)allow:(id)sender {
91  // The mouselock code expects that mouse events reach the main window
92  // immediately, but the cursor is still over the bubble, which eats the
93  // mouse events. Make the bubble transparent for mouse events.
94  if (bubbleType_ == FEB_TYPE_FULLSCREEN_MOUSELOCK_BUTTONS ||
95      bubbleType_ == FEB_TYPE_MOUSELOCK_BUTTONS)
96    [[self window] setIgnoresMouseEvents:YES];
97
98  DCHECK(fullscreen_bubble::ShowButtonsForType(bubbleType_));
99  browser_->fullscreen_controller()->OnAcceptFullscreenPermission();
100}
101
102- (void)deny:(id)sender {
103  DCHECK(fullscreen_bubble::ShowButtonsForType(bubbleType_));
104  browser_->fullscreen_controller()->OnDenyFullscreenPermission();
105}
106
107- (void)showButtons:(BOOL)show {
108  [allowButton_ setHidden:!show];
109  [denyButton_ setHidden:!show];
110  [exitLabel_ setHidden:show];
111}
112
113// We want this to be a child of a browser window.  addChildWindow:
114// (called from this function) will bring the window on-screen;
115// unfortunately, [NSWindowController showWindow:] will also bring it
116// on-screen (but will cause unexpected changes to the window's
117// position).  We cannot have an addChildWindow: and a subsequent
118// showWindow:. Thus, we have our own version.
119- (void)showWindow {
120  // Completes nib load.
121  InfoBubbleWindow* info_bubble = static_cast<InfoBubbleWindow*>([self window]);
122  [info_bubble setCanBecomeKeyWindow:NO];
123  if (!fullscreen_bubble::ShowButtonsForType(bubbleType_)) {
124    [self showButtons:NO];
125    [self hideSoon];
126  }
127  [tweaker_ tweakUI:info_bubble];
128  [[owner_ window] addChildWindow:info_bubble ordered:NSWindowAbove];
129  [owner_ layoutSubviews];
130
131  [info_bubble orderFront:self];
132}
133
134- (void)awakeFromNib {
135  DCHECK([[self window] isKindOfClass:[InfoBubbleWindow class]]);
136  [messageLabel_ setStringValue:[self getLabelText]];
137  [self initializeLabel];
138}
139
140- (void)positionInWindowAtTop:(CGFloat)maxY width:(CGFloat)maxWidth {
141  NSRect windowFrame = [self window].frame;
142  NSRect ownerWindowFrame = [owner_ window].frame;
143  NSPoint origin;
144  origin.x = ownerWindowFrame.origin.x +
145      (int)(NSWidth(ownerWindowFrame)/2 - NSWidth(windowFrame)/2);
146  origin.y = ownerWindowFrame.origin.y + maxY - NSHeight(windowFrame);
147  [[self window] setFrameOrigin:origin];
148}
149
150// Called when someone clicks on the embedded link.
151- (BOOL) textView:(NSTextView*)textView
152    clickedOnLink:(id)link
153          atIndex:(NSUInteger)charIndex {
154  browser_->fullscreen_controller()->
155      ExitTabOrBrowserFullscreenToPreviousState();
156  return YES;
157}
158
159- (void)hideTimerFired:(NSTimer*)timer {
160  // This might fire racily for buttoned bubbles, even though the timer is
161  // cancelled for them. Explicitly check for this case.
162  if (fullscreen_bubble::ShowButtonsForType(bubbleType_))
163    return;
164
165  [NSAnimationContext beginGrouping];
166  [[NSAnimationContext currentContext]
167      gtm_setDuration:kHideDuration
168            eventMask:NSLeftMouseUpMask|NSLeftMouseDownMask];
169  [[[self window] animator] setAlphaValue:0.0];
170  [NSAnimationContext endGrouping];
171}
172
173- (void)animationDidEnd:(NSAnimation*)animation {
174  if (animation == hideAnimation_.get()) {
175    hideAnimation_.reset();
176  }
177}
178
179- (void)closeImmediately {
180  // Without this, quitting fullscreen with esc will let the bubble reappear
181  // once the "exit fullscreen" animation is done on lion.
182  InfoBubbleWindow* infoBubble = static_cast<InfoBubbleWindow*>([self window]);
183  [[infoBubble parentWindow] removeChildWindow:infoBubble];
184  [hideAnimation_.get() stopAnimation];
185  [hideTimer_ invalidate];
186  [infoBubble setAllowedAnimations:info_bubble::kAnimateNone];
187  [self close];
188}
189
190- (void)dealloc {
191  [hideAnimation_.get() stopAnimation];
192  [hideTimer_ invalidate];
193  [super dealloc];
194}
195
196@end
197
198@implementation FullscreenExitBubbleController (PrivateMethods)
199
200- (void)initializeLabel {
201  // Replace the label placeholder NSTextField with the real label NSTextView.
202  // The former doesn't show links in a nice way, but the latter can't be added
203  // in IB without a containing scroll view, so create the NSTextView
204  // programmatically.
205  exitLabel_.reset([[OneClickHyperlinkTextView alloc]
206      initWithFrame:[exitLabelPlaceholder_ frame]]);
207  [exitLabel_.get() setAutoresizingMask:
208      [exitLabelPlaceholder_ autoresizingMask]];
209  [exitLabel_.get() setHidden:[exitLabelPlaceholder_ isHidden]];
210  [[exitLabelPlaceholder_ superview]
211      replaceSubview:exitLabelPlaceholder_ with:exitLabel_.get()];
212  exitLabelPlaceholder_ = nil;  // Now released.
213  [exitLabel_.get() setDelegate:self];
214
215  NSString* exitLinkText;
216  NSString* exitUnlinkedText;
217  if (bubbleType_ == FEB_TYPE_FULLSCREEN_MOUSELOCK_EXIT_INSTRUCTION ||
218      bubbleType_ == FEB_TYPE_MOUSELOCK_EXIT_INSTRUCTION) {
219    exitLinkText = @"";
220    exitUnlinkedText = [@" " stringByAppendingString:
221        l10n_util::GetNSStringF(IDS_FULLSCREEN_PRESS_ESC_TO_EXIT,
222                                l10n_util::GetStringUTF16(IDS_APP_ESC_KEY))];
223  } else {
224    exitLinkText = l10n_util::GetNSString(IDS_EXIT_FULLSCREEN_MODE);
225    exitUnlinkedText = [@" " stringByAppendingString:
226        l10n_util::GetNSStringF(IDS_EXIT_FULLSCREEN_MODE_ACCELERATOR,
227                                l10n_util::GetStringUTF16(IDS_APP_ESC_KEY))];
228  }
229
230  NSFont* font = [NSFont systemFontOfSize:
231      [NSFont systemFontSizeForControlSize:NSRegularControlSize]];
232  [(HyperlinkTextView*)exitLabel_.get()
233        setMessageAndLink:exitUnlinkedText
234                 withLink:exitLinkText
235                 atOffset:0
236                     font:font
237             messageColor:[NSColor blackColor]
238                linkColor:[NSColor blueColor]];
239  [exitLabel_.get() setAlignment:NSRightTextAlignment];
240
241  NSRect labelFrame = [exitLabel_ frame];
242
243  // NSTextView's sizeToFit: method seems to enjoy wrapping lines. Temporarily
244  // set the size large to force it not to.
245  NSRect windowFrame = [[self window] frame];
246  [exitLabel_ setFrameSize:windowFrame.size];
247  NSLayoutManager* layoutManager = [exitLabel_ layoutManager];
248  NSTextContainer* textContainer = [exitLabel_ textContainer];
249  [layoutManager ensureLayoutForTextContainer:textContainer];
250  NSRect textFrame = [layoutManager usedRectForTextContainer:textContainer];
251
252  textFrame.size.width = ceil(NSWidth(textFrame));
253  labelFrame.origin.x += NSWidth(labelFrame) - NSWidth(textFrame);
254  labelFrame.size = textFrame.size;
255  [exitLabel_ setFrame:labelFrame];
256}
257
258- (NSString*)getLabelText {
259  if (bubbleType_ == FEB_TYPE_NONE)
260    return @"";
261  ExtensionService* extension_service = extensions::ExtensionSystem::Get(
262      browser_->profile())->extension_service();
263  return SysUTF16ToNSString(fullscreen_bubble::GetLabelTextForType(
264          bubbleType_, url_, extension_service));
265}
266
267// This looks at the Main Menu and determines what the user has set as the
268// key combination for quit. It then gets the modifiers and builds an object
269// to hold the data.
270+ (scoped_ptr<ui::PlatformAcceleratorCocoa>)acceleratorForToggleFullscreen {
271  NSMenu* mainMenu = [NSApp mainMenu];
272  // Get the application menu (i.e. Chromium).
273  for (NSMenuItem* menu in [mainMenu itemArray]) {
274    for (NSMenuItem* item in [[menu submenu] itemArray]) {
275      // Find the toggle presentation mode item.
276      if ([item tag] == IDC_PRESENTATION_MODE) {
277        return scoped_ptr<ui::PlatformAcceleratorCocoa>(
278          new ui::PlatformAcceleratorCocoa([item keyEquivalent],
279                                           [item keyEquivalentModifierMask]));
280      }
281    }
282  }
283  // Default to Cmd+Shift+F.
284  return scoped_ptr<ui::PlatformAcceleratorCocoa>(
285      new ui::PlatformAcceleratorCocoa(@"f", NSCommandKeyMask|NSShiftKeyMask));
286}
287
288// This looks at the Main Menu and determines what the user has set as the
289// key combination for quit. It then gets the modifiers and builds a string
290// to display them.
291+ (NSString*)keyCommandString {
292  scoped_ptr<ui::PlatformAcceleratorCocoa> accelerator(
293      [[self class] acceleratorForToggleFullscreen]);
294  return [[self class] keyCombinationForAccelerator:*accelerator];
295}
296
297+ (NSString*)keyCombinationForAccelerator:
298    (const ui::PlatformAcceleratorCocoa&)item {
299  NSMutableString* string = [NSMutableString string];
300  NSUInteger modifiers = item.modifier_mask();
301
302  if (modifiers & NSCommandKeyMask)
303    [string appendString:@"\u2318"];
304  if (modifiers & NSControlKeyMask)
305    [string appendString:@"\u2303"];
306  if (modifiers & NSAlternateKeyMask)
307    [string appendString:@"\u2325"];
308  BOOL isUpperCase = [[NSCharacterSet uppercaseLetterCharacterSet]
309      characterIsMember:[item.characters() characterAtIndex:0]];
310  if (modifiers & NSShiftKeyMask || isUpperCase)
311    [string appendString:@"\u21E7"];
312
313  [string appendString:[item.characters() uppercaseString]];
314  return string;
315}
316
317- (void)hideSoon {
318  hideTimer_.reset(
319      [[NSTimer scheduledTimerWithTimeInterval:kInitialDelay
320                                        target:self
321                                      selector:@selector(hideTimerFired:)
322                                      userInfo:nil
323                                       repeats:NO] retain]);
324}
325
326@end
327