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