blob: 5683b1bcfe94d4338914692089fb4c994717ec24 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <Cocoa/Cocoa.h>
#include "base/logging.h" // for NOTREACHED()
#include "base/mac/bundle_locations.h"
#include "base/mac/mac_util.h"
#include "base/sys_string_conversions.h"
#include "base/utf_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/ui/browser.h"
#import "chrome/browser/ui/cocoa/browser_window_controller.h"
#include "chrome/browser/ui/cocoa/event_utils.h"
#import "chrome/browser/ui/cocoa/fullscreen_exit_bubble_controller.h"
#import "chrome/browser/ui/cocoa/hyperlink_text_view.h"
#import "chrome/browser/ui/cocoa/info_bubble_view.h"
#import "chrome/browser/ui/cocoa/info_bubble_window.h"
#include "chrome/browser/ui/fullscreen_exit_bubble_type.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
#include "grit/generated_resources.h"
#include "grit/ui_strings.h"
#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
#include "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
#import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
#include "ui/base/accelerators/accelerator_cocoa.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"
namespace {
const float kInitialDelay = 3.8;
const float kHideDuration = 0.7;
} // namespace
@interface OneClickHyperlinkTextView : HyperlinkTextView
@implementation OneClickHyperlinkTextView
- (BOOL)acceptsFirstMouse:(NSEvent*)event {
return YES;
@interface FullscreenExitBubbleController (PrivateMethods)
// Sets |exitLabel_| based on |exitLabelPlaceholder_|,
// sets |exitLabelPlaceholder_| to nil.
- (void)initializeLabel;
- (NSString*)getLabelText;
- (void)hideSoon;
// Returns the Accelerator for the Toggle Fullscreen menu item.
+ (ui::AcceleratorCocoa)acceleratorForToggleFullscreen;
// Returns a string representation fit for display of
// +acceleratorForToggleFullscreen.
+ (NSString*)keyCommandString;
+ (NSString*)keyCombinationForAccelerator:(const ui::AcceleratorCocoa&)item;
@implementation FullscreenExitBubbleController
- (id)initWithOwner:(BrowserWindowController*)owner
url:(const GURL&)url
bubbleType:(FullscreenExitBubbleType)bubbleType {
NSString* nibPath =
[base::mac::FrameworkBundle() pathForResource:@"FullscreenExitBubble"
if ((self = [super initWithWindowNibPath:nibPath owner:self])) {
browser_ = browser;
owner_ = owner;
url_ = url;
bubbleType_ = bubbleType;
return self;
- (void)allow:(id)sender {
// The mouselock code expects that mouse events reach the main window
// immediately, but the cursor is still over the bubble, which eats the
// mouse events. Make the bubble transparent for mouse events.
[[self window] setIgnoresMouseEvents:YES];
url_, bubbleType_);
[self showButtons:NO];
[self hideSoon];
- (void)deny:(id)sender {
- (void)showButtons:(BOOL)show {
[allowButton_ setHidden:!show];
[denyButton_ setHidden:!show];
[exitLabel_ setHidden:show];
// We want this to be a child of a browser window. addChildWindow:
// (called from this function) will bring the window on-screen;
// unfortunately, [NSWindowController showWindow:] will also bring it
// on-screen (but will cause unexpected changes to the window's
// position). We cannot have an addChildWindow: and a subsequent
// showWindow:. Thus, we have our own version.
- (void)showWindow {
// Completes nib load.
InfoBubbleWindow* info_bubble = static_cast<InfoBubbleWindow*>([self window]);
[info_bubble setCanBecomeKeyWindow:NO];
if (!fullscreen_bubble::ShowButtonsForType(bubbleType_)) {
[self showButtons:NO];
[self hideSoon];
[tweaker_ tweakUI:info_bubble];
[[owner_ window] addChildWindow:info_bubble ordered:NSWindowAbove];
[owner_ layoutSubviews];
[info_bubble orderFront:self];
- (void)awakeFromNib {
DCHECK([[self window] isKindOfClass:[InfoBubbleWindow class]]);
[messageLabel_ setStringValue:[self getLabelText]];
[self initializeLabel];
- (void)positionInWindowAtTop:(CGFloat)maxY width:(CGFloat)maxWidth {
NSRect windowFrame = [self window].frame;
NSRect ownerWindowFrame = [owner_ window].frame;
NSPoint origin;
origin.x = ownerWindowFrame.origin.x +
(int)(NSWidth(ownerWindowFrame)/2 - NSWidth(windowFrame)/2);
origin.y = ownerWindowFrame.origin.y + maxY - NSHeight(windowFrame);
[[self window] setFrameOrigin:origin];
- (void)updateURL:(const GURL&)url
bubbleType:(FullscreenExitBubbleType)bubbleType {
bubbleType_ = bubbleType;
[messageLabel_ setStringValue:[self getLabelText]];
// Make sure the bubble is visible.
[hideAnimation_.get() stopAnimation];
[hideTimer_ invalidate];
[[[self window] animator] setAlphaValue:1.0];
if (fullscreen_bubble::ShowButtonsForType(bubbleType)) {
[denyButton_ setTitle:SysUTF16ToNSString(
[self showButtons:YES];
// Reenable mouse events if they were disabled previously.
[[self window] setIgnoresMouseEvents:NO];
} else {
[self showButtons:NO];
// Only button-less bubbles auto-hide.
[self hideSoon];
// TODO(jeremya): show "Press Esc to exit" instead of a link on mouselock.
// Relayout. A bit jumpy, but functional.
[tweaker_ tweakUI:[self window]];
[owner_ layoutSubviews];
// Called when someone clicks on the embedded link.
- (BOOL) textView:(NSTextView*)textView
atIndex:(NSUInteger)charIndex {
return YES;
- (void)hideTimerFired:(NSTimer*)timer {
// This might fire racily for buttoned bubbles, even though the timer is
// cancelled for them. Explicitly check for this case.
if (fullscreen_bubble::ShowButtonsForType(bubbleType_))
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext]
[[[self window] animator] setAlphaValue:0.0];
[NSAnimationContext endGrouping];
- (void)animationDidEnd:(NSAnimation*)animation {
if (animation == hideAnimation_.get()) {
- (void)closeImmediately {
// Without this, quitting fullscreen with esc will let the bubble reappear
// once the "exit fullscreen" animation is done on lion.
InfoBubbleWindow* infoBubble = static_cast<InfoBubbleWindow*>([self window]);
[[infoBubble parentWindow] removeChildWindow:infoBubble];
[hideAnimation_.get() stopAnimation];
[hideTimer_ invalidate];
[infoBubble setDelayOnClose:NO];
[self close];
- (void)dealloc {
[hideAnimation_.get() stopAnimation];
[hideTimer_ invalidate];
[super dealloc];
@implementation FullscreenExitBubbleController (PrivateMethods)
- (void)initializeLabel {
// Replace the label placeholder NSTextField with the real label NSTextView.
// The former doesn't show links in a nice way, but the latter can't be added
// in IB without a containing scroll view, so create the NSTextView
// programmatically.
exitLabel_.reset([[OneClickHyperlinkTextView alloc]
initWithFrame:[exitLabelPlaceholder_ frame]]);
[exitLabel_.get() setAutoresizingMask:
[exitLabelPlaceholder_ autoresizingMask]];
[exitLabel_.get() setHidden:[exitLabelPlaceholder_ isHidden]];
[[exitLabelPlaceholder_ superview]
replaceSubview:exitLabelPlaceholder_ with:exitLabel_.get()];
exitLabelPlaceholder_ = nil; // Now released.
[exitLabel_.get() setDelegate:self];
NSString* exitLinkText = l10n_util::GetNSString(IDS_EXIT_FULLSCREEN_MODE);
NSString* acceleratorText = [@" " stringByAppendingString:
NSFont* font = [NSFont systemFontOfSize:
[NSFont systemFontSizeForControlSize:NSRegularControlSize]];
messageColor:[NSColor blackColor]
linkColor:[NSColor blueColor]];
[exitLabel_.get() setAlignment:NSRightTextAlignment];
NSRect labelFrame = [exitLabel_ frame];
// NSTextView's sizeToFit: method seems to enjoy wrapping lines. Temporarily
// set the size large to force it not to.
NSRect windowFrame = [[self window] frame];
[exitLabel_ setFrameSize:windowFrame.size];
NSLayoutManager* layoutManager = [exitLabel_ layoutManager];
NSTextContainer* textContainer = [exitLabel_ textContainer];
[layoutManager ensureLayoutForTextContainer:textContainer];
NSRect textFrame = [layoutManager usedRectForTextContainer:textContainer];
textFrame.size.width = ceil(NSWidth(textFrame));
labelFrame.origin.x += NSWidth(labelFrame) - NSWidth(textFrame);
labelFrame.size = textFrame.size;
[exitLabel_ setFrame:labelFrame];
- (NSString*)getLabelText {
if (bubbleType_ == FEB_TYPE_NONE)
return @"";
return SysUTF16ToNSString(fullscreen_bubble::GetLabelTextForType(
bubbleType_, url_, browser_->profile()->GetExtensionService()));
// This looks at the Main Menu and determines what the user has set as the
// key combination for quit. It then gets the modifiers and builds an object
// to hold the data.
+ (ui::AcceleratorCocoa)acceleratorForToggleFullscreen {
NSMenu* mainMenu = [NSApp mainMenu];
// Get the application menu (i.e. Chromium).
for (NSMenuItem* menu in [mainMenu itemArray]) {
for (NSMenuItem* item in [[menu submenu] itemArray]) {
// Find the toggle presentation mode item.
if ([item tag] == IDC_PRESENTATION_MODE) {
return ui::AcceleratorCocoa([item keyEquivalent],
[item keyEquivalentModifierMask]);
// Default to Cmd+Shift+F.
return ui::AcceleratorCocoa(@"f", NSCommandKeyMask|NSShiftKeyMask);
// This looks at the Main Menu and determines what the user has set as the
// key combination for quit. It then gets the modifiers and builds a string
// to display them.
+ (NSString*)keyCommandString {
ui::AcceleratorCocoa accelerator =
[[self class] acceleratorForToggleFullscreen];
return [[self class] keyCombinationForAccelerator:accelerator];
+ (NSString*)keyCombinationForAccelerator:(const ui::AcceleratorCocoa&)item {
NSMutableString* string = [NSMutableString string];
NSUInteger modifiers = item.modifiers();
if (modifiers & NSCommandKeyMask)
[string appendString:@"\u2318"];
if (modifiers & NSControlKeyMask)
[string appendString:@"\u2303"];
if (modifiers & NSAlternateKeyMask)
[string appendString:@"\u2325"];
BOOL isUpperCase = [[NSCharacterSet uppercaseLetterCharacterSet]
characterIsMember:[item.characters() characterAtIndex:0]];
if (modifiers & NSShiftKeyMask || isUpperCase)
[string appendString:@"\u21E7"];
[string appendString:[item.characters() uppercaseString]];
return string;
- (void)hideSoon {
[[NSTimer scheduledTimerWithTimeInterval:kInitialDelay
repeats:NO] retain]);