1// Copyright (c) 2011 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#include "chrome/browser/ui/cocoa/browser_window_cocoa.h" 6 7#include "base/command_line.h" 8#include "base/logging.h" 9#include "base/message_loop.h" 10#include "base/sys_string_conversions.h" 11#include "chrome/app/chrome_command_ids.h" 12#include "chrome/browser/bookmarks/bookmark_utils.h" 13#include "chrome/browser/download/download_shelf.h" 14#include "chrome/browser/global_keyboard_shortcuts_mac.h" 15#include "chrome/browser/page_info_window.h" 16#include "chrome/browser/prefs/pref_service.h" 17#include "chrome/browser/profiles/profile.h" 18#include "chrome/browser/sidebar/sidebar_container.h" 19#include "chrome/browser/sidebar/sidebar_manager.h" 20#include "chrome/browser/ui/browser.h" 21#include "chrome/browser/ui/browser_list.h" 22#import "chrome/browser/ui/cocoa/browser_window_controller.h" 23#import "chrome/browser/ui/cocoa/bug_report_window_controller.h" 24#import "chrome/browser/ui/cocoa/chrome_event_processing_window.h" 25#import "chrome/browser/ui/cocoa/content_settings/collected_cookies_mac.h" 26#import "chrome/browser/ui/cocoa/download/download_shelf_controller.h" 27#import "chrome/browser/ui/cocoa/html_dialog_window_controller.h" 28#import "chrome/browser/ui/cocoa/location_bar/location_bar_view_mac.h" 29#import "chrome/browser/ui/cocoa/nsmenuitem_additions.h" 30#include "chrome/browser/ui/cocoa/repost_form_warning_mac.h" 31#include "chrome/browser/ui/cocoa/restart_browser.h" 32#include "chrome/browser/ui/cocoa/status_bubble_mac.h" 33#include "chrome/browser/ui/cocoa/task_manager_mac.h" 34#import "chrome/browser/ui/cocoa/theme_install_bubble_view.h" 35#import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h" 36#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" 37#include "chrome/common/pref_names.h" 38#include "content/browser/tab_contents/tab_contents.h" 39#include "content/common/native_web_keyboard_event.h" 40#include "content/common/notification_service.h" 41#include "grit/chromium_strings.h" 42#include "grit/generated_resources.h" 43#include "ui/base/l10n/l10n_util_mac.h" 44#include "ui/gfx/rect.h" 45 46BrowserWindowCocoa::BrowserWindowCocoa(Browser* browser, 47 BrowserWindowController* controller, 48 NSWindow* window) 49 : browser_(browser), 50 controller_(controller), 51 confirm_close_factory_(browser) { 52 // This pref applies to all windows, so all must watch for it. 53 registrar_.Add(this, NotificationType::BOOKMARK_BAR_VISIBILITY_PREF_CHANGED, 54 NotificationService::AllSources()); 55 registrar_.Add(this, NotificationType::SIDEBAR_CHANGED, 56 NotificationService::AllSources()); 57} 58 59BrowserWindowCocoa::~BrowserWindowCocoa() { 60} 61 62void BrowserWindowCocoa::Show() { 63 // The Browser associated with this browser window must become the active 64 // browser at the time |Show()| is called. This is the natural behaviour under 65 // Windows, but |-makeKeyAndOrderFront:| won't send |-windowDidBecomeMain:| 66 // until we return to the runloop. Therefore any calls to 67 // |BrowserList::GetLastActive()| (for example, in bookmark_util), will return 68 // the previous browser instead if we don't explicitly set it here. 69 BrowserList::SetLastActive(browser_); 70 71 [window() makeKeyAndOrderFront:controller_]; 72} 73 74void BrowserWindowCocoa::ShowInactive() { 75 [window() orderFront:controller_]; 76} 77 78void BrowserWindowCocoa::SetBounds(const gfx::Rect& bounds) { 79 SetFullscreen(false); 80 NSRect cocoa_bounds = NSMakeRect(bounds.x(), 0, bounds.width(), 81 bounds.height()); 82 // Flip coordinates based on the primary screen. 83 NSScreen* screen = [[NSScreen screens] objectAtIndex:0]; 84 cocoa_bounds.origin.y = 85 [screen frame].size.height - bounds.height() - bounds.y(); 86 87 [window() setFrame:cocoa_bounds display:YES]; 88} 89 90// Callers assume that this doesn't immediately delete the Browser object. 91// The controller implementing the window delegate methods called from 92// |-performClose:| must take precautions to ensure that. 93void BrowserWindowCocoa::Close() { 94 // If there is an overlay window, we contain a tab being dragged between 95 // windows. Don't hide the window as it makes the UI extra confused. We can 96 // still close the window, as that will happen when the drag completes. 97 if ([controller_ overlayWindow]) { 98 [controller_ deferPerformClose]; 99 } else { 100 // Make sure we hide the window immediately. Even though performClose: 101 // calls orderOut: eventually, it leaves the window on-screen long enough 102 // that we start to see tabs shutting down. http://crbug.com/23959 103 // TODO(viettrungluu): This is kind of bad, since |-performClose:| calls 104 // |-windowShouldClose:| (on its delegate, which is probably the 105 // controller) which may return |NO| causing the window to not be closed, 106 // thereby leaving a hidden window. In fact, our window-closing procedure 107 // involves a (indirect) recursion on |-performClose:|, which is also bad. 108 [window() orderOut:controller_]; 109 [window() performClose:controller_]; 110 } 111} 112 113void BrowserWindowCocoa::Activate() { 114 [controller_ activate]; 115} 116 117void BrowserWindowCocoa::Deactivate() { 118 // TODO(jcivelli): http://crbug.com/51364 Implement me. 119 NOTIMPLEMENTED(); 120} 121 122void BrowserWindowCocoa::FlashFrame() { 123 [NSApp requestUserAttention:NSInformationalRequest]; 124} 125 126bool BrowserWindowCocoa::IsActive() const { 127 return [window() isKeyWindow]; 128} 129 130gfx::NativeWindow BrowserWindowCocoa::GetNativeHandle() { 131 return window(); 132} 133 134BrowserWindowTesting* BrowserWindowCocoa::GetBrowserWindowTesting() { 135 return NULL; 136} 137 138StatusBubble* BrowserWindowCocoa::GetStatusBubble() { 139 return [controller_ statusBubble]; 140} 141 142void BrowserWindowCocoa::ToolbarSizeChanged(bool is_animating) { 143 // According to beng, this is an ugly method that comes from the days when the 144 // download shelf was a ChromeView attached to the TabContents, and as its 145 // size changed via animation it notified through TCD/etc to the browser view 146 // to relayout for each tick of the animation. We don't need anything of the 147 // sort on Mac. 148} 149 150void BrowserWindowCocoa::UpdateTitleBar() { 151 NSString* newTitle = 152 base::SysUTF16ToNSString(browser_->GetWindowTitleForCurrentTab()); 153 154 // Work around Cocoa bug: if a window changes title during the tracking of the 155 // Window menu it doesn't display well and the constant re-sorting of the list 156 // makes it difficult for the user to pick the desired window. Delay window 157 // title updates until the default run-loop mode. 158 159 if (pending_window_title_.get()) 160 [[NSRunLoop currentRunLoop] 161 cancelPerformSelector:@selector(setTitle:) 162 target:window() 163 argument:pending_window_title_.get()]; 164 165 pending_window_title_.reset([newTitle copy]); 166 [[NSRunLoop currentRunLoop] 167 performSelector:@selector(setTitle:) 168 target:window() 169 argument:newTitle 170 order:0 171 modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]]; 172} 173 174void BrowserWindowCocoa::ShelfVisibilityChanged() { 175 // Mac doesn't yet support showing the bookmark bar at a different size on 176 // the new tab page. When it does, this method should attempt to relayout the 177 // bookmark bar/extension shelf as their preferred height may have changed. 178 // http://crbug.com/43346 179} 180 181void BrowserWindowCocoa::UpdateDevTools() { 182 [controller_ updateDevToolsForContents: 183 browser_->GetSelectedTabContents()]; 184} 185 186void BrowserWindowCocoa::UpdateLoadingAnimations(bool should_animate) { 187 // Do nothing on Mac. 188} 189 190void BrowserWindowCocoa::SetStarredState(bool is_starred) { 191 [controller_ setStarredState:is_starred ? YES : NO]; 192} 193 194gfx::Rect BrowserWindowCocoa::GetRestoredBounds() const { 195 // Flip coordinates based on the primary screen. 196 NSScreen* screen = [[NSScreen screens] objectAtIndex:0]; 197 NSRect frame = [controller_ regularWindowFrame]; 198 gfx::Rect bounds(frame.origin.x, 0, frame.size.width, frame.size.height); 199 bounds.set_y([screen frame].size.height - frame.origin.y - frame.size.height); 200 return bounds; 201} 202 203gfx::Rect BrowserWindowCocoa::GetBounds() const { 204 return GetRestoredBounds(); 205} 206 207bool BrowserWindowCocoa::IsMaximized() const { 208 return [window() isZoomed]; 209} 210 211void BrowserWindowCocoa::SetFullscreen(bool fullscreen) { 212 [controller_ setFullscreen:fullscreen]; 213} 214 215bool BrowserWindowCocoa::IsFullscreen() const { 216 return !![controller_ isFullscreen]; 217} 218 219bool BrowserWindowCocoa::IsFullscreenBubbleVisible() const { 220 return false; 221} 222 223void BrowserWindowCocoa::ConfirmAddSearchProvider( 224 const TemplateURL* template_url, 225 Profile* profile) { 226 NOTIMPLEMENTED(); 227} 228 229LocationBar* BrowserWindowCocoa::GetLocationBar() const { 230 return [controller_ locationBarBridge]; 231} 232 233void BrowserWindowCocoa::SetFocusToLocationBar(bool select_all) { 234 [controller_ focusLocationBar:select_all ? YES : NO]; 235} 236 237void BrowserWindowCocoa::UpdateReloadStopState(bool is_loading, bool force) { 238 [controller_ setIsLoading:is_loading force:force]; 239} 240 241void BrowserWindowCocoa::UpdateToolbar(TabContentsWrapper* contents, 242 bool should_restore_state) { 243 [controller_ updateToolbarWithContents:contents->tab_contents() 244 shouldRestoreState:should_restore_state ? YES : NO]; 245} 246 247void BrowserWindowCocoa::FocusToolbar() { 248 // Not needed on the Mac. 249} 250 251void BrowserWindowCocoa::FocusAppMenu() { 252 // Chrome uses the standard Mac OS X menu bar, so this isn't needed. 253} 254 255void BrowserWindowCocoa::RotatePaneFocus(bool forwards) { 256 // Not needed on the Mac. 257} 258 259void BrowserWindowCocoa::FocusBookmarksToolbar() { 260 // Not needed on the Mac. 261} 262 263void BrowserWindowCocoa::FocusChromeOSStatus() { 264 // Not needed on the Mac. 265} 266 267bool BrowserWindowCocoa::IsBookmarkBarVisible() const { 268 return (browser_->profile()->GetPrefs()->GetBoolean( 269 prefs::kShowBookmarkBar) && 270 browser_->profile()->GetPrefs()->GetBoolean( 271 prefs::kEnableBookmarkBar)); 272} 273 274bool BrowserWindowCocoa::IsBookmarkBarAnimating() const { 275 return [controller_ isBookmarkBarAnimating]; 276} 277 278bool BrowserWindowCocoa::IsTabStripEditable() const { 279 return ![controller_ isDragSessionActive]; 280} 281 282bool BrowserWindowCocoa::IsToolbarVisible() const { 283 return browser_->SupportsWindowFeature(Browser::FEATURE_TOOLBAR) || 284 browser_->SupportsWindowFeature(Browser::FEATURE_LOCATIONBAR); 285} 286 287// This is called from Browser, which in turn is called directly from 288// a menu option. All we do here is set a preference. The act of 289// setting the preference sends notifications to all windows who then 290// know what to do. 291void BrowserWindowCocoa::ToggleBookmarkBar() { 292 bookmark_utils::ToggleWhenVisible(browser_->profile()); 293} 294 295void BrowserWindowCocoa::AddFindBar( 296 FindBarCocoaController* find_bar_cocoa_controller) { 297 return [controller_ addFindBar:find_bar_cocoa_controller]; 298} 299 300void BrowserWindowCocoa::ShowAboutChromeDialog() { 301 // Go through AppController's implementation to bring up the branded panel. 302 [[NSApp delegate] orderFrontStandardAboutPanel:nil]; 303} 304 305void BrowserWindowCocoa::ShowUpdateChromeDialog() { 306 restart_browser::RequestRestart(window()); 307} 308 309void BrowserWindowCocoa::ShowTaskManager() { 310 TaskManagerMac::Show(false); 311} 312 313void BrowserWindowCocoa::ShowBackgroundPages() { 314 TaskManagerMac::Show(true); 315} 316 317void BrowserWindowCocoa::ShowBookmarkBubble(const GURL& url, 318 bool already_bookmarked) { 319 [controller_ showBookmarkBubbleForURL:url 320 alreadyBookmarked:(already_bookmarked ? YES : NO)]; 321} 322 323bool BrowserWindowCocoa::IsDownloadShelfVisible() const { 324 return [controller_ isDownloadShelfVisible] != NO; 325} 326 327DownloadShelf* BrowserWindowCocoa::GetDownloadShelf() { 328 DownloadShelfController* shelfController = [controller_ downloadShelf]; 329 return [shelfController bridge]; 330} 331 332void BrowserWindowCocoa::ShowRepostFormWarningDialog( 333 TabContents* tab_contents) { 334 RepostFormWarningMac::Create(GetNativeHandle(), tab_contents); 335} 336 337void BrowserWindowCocoa::ShowCollectedCookiesDialog(TabContents* tab_contents) { 338 // Deletes itself on close. 339 new CollectedCookiesMac(GetNativeHandle(), tab_contents); 340} 341 342void BrowserWindowCocoa::ShowThemeInstallBubble() { 343 ThemeInstallBubbleView::Show(window()); 344} 345 346// We allow closing the window here since the real quit decision on Mac is made 347// in [AppController quit:]. 348void BrowserWindowCocoa::ConfirmBrowserCloseWithPendingDownloads() { 349 // Call InProgressDownloadResponse asynchronously to avoid a crash when the 350 // browser window is closed here (http://crbug.com/44454). 351 MessageLoop::current()->PostTask( 352 FROM_HERE, 353 confirm_close_factory_.NewRunnableMethod( 354 &Browser::InProgressDownloadResponse, 355 true)); 356} 357 358void BrowserWindowCocoa::ShowHTMLDialog(HtmlDialogUIDelegate* delegate, 359 gfx::NativeWindow parent_window) { 360 [HtmlDialogWindowController showHtmlDialog:delegate 361 profile:browser_->profile()]; 362} 363 364void BrowserWindowCocoa::UserChangedTheme() { 365 [controller_ userChangedTheme]; 366} 367 368int BrowserWindowCocoa::GetExtraRenderViewHeight() const { 369 // Currently this is only used on linux. 370 return 0; 371} 372 373void BrowserWindowCocoa::TabContentsFocused(TabContents* tab_contents) { 374 NOTIMPLEMENTED(); 375} 376 377void BrowserWindowCocoa::ShowPageInfo(Profile* profile, 378 const GURL& url, 379 const NavigationEntry::SSLStatus& ssl, 380 bool show_history) { 381 browser::ShowPageInfoBubble(window(), profile, url, ssl, show_history); 382} 383 384void BrowserWindowCocoa::ShowAppMenu() { 385 // No-op. Mac doesn't support showing the menus via alt keys. 386} 387 388bool BrowserWindowCocoa::PreHandleKeyboardEvent( 389 const NativeWebKeyboardEvent& event, bool* is_keyboard_shortcut) { 390 if (event.skip_in_browser || event.type == NativeWebKeyboardEvent::Char) 391 return false; 392 393 DCHECK(event.os_event != NULL); 394 int id = GetCommandId(event); 395 if (id == -1) 396 return false; 397 398 if (browser_->IsReservedCommandOrKey(id, event)) 399 return HandleKeyboardEventInternal(event.os_event); 400 401 DCHECK(is_keyboard_shortcut != NULL); 402 *is_keyboard_shortcut = true; 403 404 return false; 405} 406 407void BrowserWindowCocoa::HandleKeyboardEvent( 408 const NativeWebKeyboardEvent& event) { 409 if (event.skip_in_browser || event.type == NativeWebKeyboardEvent::Char) 410 return; 411 412 DCHECK(event.os_event != NULL); 413 HandleKeyboardEventInternal(event.os_event); 414} 415 416@interface MenuWalker : NSObject 417+ (NSMenuItem*)itemForKeyEquivalent:(NSEvent*)key 418 menu:(NSMenu*)menu; 419@end 420 421@implementation MenuWalker 422+ (NSMenuItem*)itemForKeyEquivalent:(NSEvent*)key 423 menu:(NSMenu*)menu { 424 NSMenuItem* result = nil; 425 426 for (NSMenuItem *item in [menu itemArray]) { 427 NSMenu* submenu = [item submenu]; 428 if (submenu) { 429 if (submenu != [NSApp servicesMenu]) 430 result = [self itemForKeyEquivalent:key 431 menu:submenu]; 432 } else if ([item cr_firesForKeyEventIfEnabled:key]) { 433 result = item; 434 } 435 436 if (result) 437 break; 438 } 439 440 return result; 441} 442@end 443 444int BrowserWindowCocoa::GetCommandId(const NativeWebKeyboardEvent& event) { 445 if ([event.os_event type] != NSKeyDown) 446 return -1; 447 448 // Look in menu. 449 NSMenuItem* item = [MenuWalker itemForKeyEquivalent:event.os_event 450 menu:[NSApp mainMenu]]; 451 452 if (item && [item action] == @selector(commandDispatch:) && [item tag] > 0) 453 return [item tag]; 454 455 // "Close window" doesn't use the |commandDispatch:| mechanism. Menu items 456 // that do not correspond to IDC_ constants need no special treatment however, 457 // as they can't be blacklisted in |Browser::IsReservedCommandOrKey()| anyhow. 458 if (item && [item action] == @selector(performClose:)) 459 return IDC_CLOSE_WINDOW; 460 461 // "Exit" doesn't use the |commandDispatch:| mechanism either. 462 if (item && [item action] == @selector(terminate:)) 463 return IDC_EXIT; 464 465 // Look in secondary keyboard shortcuts. 466 NSUInteger modifiers = [event.os_event modifierFlags]; 467 const bool cmdKey = (modifiers & NSCommandKeyMask) != 0; 468 const bool shiftKey = (modifiers & NSShiftKeyMask) != 0; 469 const bool cntrlKey = (modifiers & NSControlKeyMask) != 0; 470 const bool optKey = (modifiers & NSAlternateKeyMask) != 0; 471 const int keyCode = [event.os_event keyCode]; 472 const unichar keyChar = KeyCharacterForEvent(event.os_event); 473 474 int cmdNum = CommandForWindowKeyboardShortcut( 475 cmdKey, shiftKey, cntrlKey, optKey, keyCode, keyChar); 476 if (cmdNum != -1) 477 return cmdNum; 478 479 cmdNum = CommandForBrowserKeyboardShortcut( 480 cmdKey, shiftKey, cntrlKey, optKey, keyCode, keyChar); 481 if (cmdNum != -1) 482 return cmdNum; 483 484 return -1; 485} 486 487bool BrowserWindowCocoa::HandleKeyboardEventInternal(NSEvent* event) { 488 ChromeEventProcessingWindow* event_window = 489 static_cast<ChromeEventProcessingWindow*>(window()); 490 DCHECK([event_window isKindOfClass:[ChromeEventProcessingWindow class]]); 491 492 // Do not fire shortcuts on key up. 493 if ([event type] == NSKeyDown) { 494 // Send the event to the menu before sending it to the browser/window 495 // shortcut handling, so that if a user configures cmd-left to mean 496 // "previous tab", it takes precedence over the built-in "history back" 497 // binding. Other than that, the |-redispatchKeyEvent:| call would take care 498 // of invoking the original menu item shortcut as well. 499 500 if ([[NSApp mainMenu] performKeyEquivalent:event]) 501 return true; 502 503 if ([event_window handleExtraBrowserKeyboardShortcut:event]) 504 return true; 505 506 if ([event_window handleExtraWindowKeyboardShortcut:event]) 507 return true; 508 509 if ([event_window handleDelayedWindowKeyboardShortcut:event]) 510 return true; 511 } 512 513 return [event_window redispatchKeyEvent:event]; 514} 515 516void BrowserWindowCocoa::ShowCreateWebAppShortcutsDialog( 517 TabContentsWrapper* tab_contents) { 518 NOTIMPLEMENTED(); 519} 520 521void BrowserWindowCocoa::ShowCreateChromeAppShortcutsDialog( 522 Profile* profile, const Extension* app) { 523 NOTIMPLEMENTED(); 524} 525 526void BrowserWindowCocoa::Cut() { 527 [NSApp sendAction:@selector(cut:) to:nil from:nil]; 528} 529 530void BrowserWindowCocoa::Copy() { 531 [NSApp sendAction:@selector(copy:) to:nil from:nil]; 532} 533 534void BrowserWindowCocoa::Paste() { 535 [NSApp sendAction:@selector(paste:) to:nil from:nil]; 536} 537 538void BrowserWindowCocoa::ToggleTabStripMode() { 539 [controller_ toggleTabStripDisplayMode]; 540} 541 542void BrowserWindowCocoa::OpenTabpose() { 543 [controller_ openTabpose]; 544} 545 546void BrowserWindowCocoa::PrepareForInstant() { 547 // TODO: implement fade as done on windows. 548} 549 550void BrowserWindowCocoa::ShowInstant(TabContentsWrapper* preview) { 551 [controller_ showInstant:preview->tab_contents()]; 552} 553 554void BrowserWindowCocoa::HideInstant(bool instant_is_active) { 555 [controller_ hideInstant]; 556 557 // TODO: add support for |instant_is_active|. 558} 559 560gfx::Rect BrowserWindowCocoa::GetInstantBounds() { 561 // Flip coordinates based on the primary screen. 562 NSScreen* screen = [[NSScreen screens] objectAtIndex:0]; 563 NSRect monitorFrame = [screen frame]; 564 NSRect frame = [controller_ instantFrame]; 565 gfx::Rect bounds(NSRectToCGRect(frame)); 566 bounds.set_y(NSHeight(monitorFrame) - bounds.y() - bounds.height()); 567 return bounds; 568} 569 570void BrowserWindowCocoa::Observe(NotificationType type, 571 const NotificationSource& source, 572 const NotificationDetails& details) { 573 switch (type.value) { 574 // Only the key window gets a direct toggle from the menu. 575 // Other windows hear about it from the notification. 576 case NotificationType::BOOKMARK_BAR_VISIBILITY_PREF_CHANGED: 577 [controller_ updateBookmarkBarVisibilityWithAnimation:YES]; 578 break; 579 case NotificationType::SIDEBAR_CHANGED: 580 UpdateSidebarForContents( 581 Details<SidebarContainer>(details)->tab_contents()); 582 break; 583 default: 584 NOTREACHED(); // we don't ask for anything else! 585 break; 586 } 587} 588 589void BrowserWindowCocoa::DestroyBrowser() { 590 [controller_ destroyBrowser]; 591 592 // at this point the controller is dead (autoreleased), so 593 // make sure we don't try to reference it any more. 594} 595 596NSWindow* BrowserWindowCocoa::window() const { 597 return [controller_ window]; 598} 599 600void BrowserWindowCocoa::UpdateSidebarForContents(TabContents* tab_contents) { 601 if (tab_contents == browser_->GetSelectedTabContents()) { 602 [controller_ updateSidebarForContents:tab_contents]; 603 } 604} 605