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 46 BrowserWindowCocoa::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 59 BrowserWindowCocoa::~BrowserWindowCocoa() { 60 } 61 62 void 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 74 void BrowserWindowCocoa::ShowInactive() { 75 [window() orderFront:controller_]; 76 } 77 78 void 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. 93 void 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 113 void BrowserWindowCocoa::Activate() { 114 [controller_ activate]; 115 } 116 117 void BrowserWindowCocoa::Deactivate() { 118 // TODO(jcivelli): http://crbug.com/51364 Implement me. 119 NOTIMPLEMENTED(); 120 } 121 122 void BrowserWindowCocoa::FlashFrame() { 123 [NSApp requestUserAttention:NSInformationalRequest]; 124 } 125 126 bool BrowserWindowCocoa::IsActive() const { 127 return [window() isKeyWindow]; 128 } 129 130 gfx::NativeWindow BrowserWindowCocoa::GetNativeHandle() { 131 return window(); 132 } 133 134 BrowserWindowTesting* BrowserWindowCocoa::GetBrowserWindowTesting() { 135 return NULL; 136 } 137 138 StatusBubble* BrowserWindowCocoa::GetStatusBubble() { 139 return [controller_ statusBubble]; 140 } 141 142 void 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 150 void 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 174 void 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 181 void BrowserWindowCocoa::UpdateDevTools() { 182 [controller_ updateDevToolsForContents: 183 browser_->GetSelectedTabContents()]; 184 } 185 186 void BrowserWindowCocoa::UpdateLoadingAnimations(bool should_animate) { 187 // Do nothing on Mac. 188 } 189 190 void BrowserWindowCocoa::SetStarredState(bool is_starred) { 191 [controller_ setStarredState:is_starred ? YES : NO]; 192 } 193 194 gfx::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 203 gfx::Rect BrowserWindowCocoa::GetBounds() const { 204 return GetRestoredBounds(); 205 } 206 207 bool BrowserWindowCocoa::IsMaximized() const { 208 return [window() isZoomed]; 209 } 210 211 void BrowserWindowCocoa::SetFullscreen(bool fullscreen) { 212 [controller_ setFullscreen:fullscreen]; 213 } 214 215 bool BrowserWindowCocoa::IsFullscreen() const { 216 return !![controller_ isFullscreen]; 217 } 218 219 bool BrowserWindowCocoa::IsFullscreenBubbleVisible() const { 220 return false; 221 } 222 223 void BrowserWindowCocoa::ConfirmAddSearchProvider( 224 const TemplateURL* template_url, 225 Profile* profile) { 226 NOTIMPLEMENTED(); 227 } 228 229 LocationBar* BrowserWindowCocoa::GetLocationBar() const { 230 return [controller_ locationBarBridge]; 231 } 232 233 void BrowserWindowCocoa::SetFocusToLocationBar(bool select_all) { 234 [controller_ focusLocationBar:select_all ? YES : NO]; 235 } 236 237 void BrowserWindowCocoa::UpdateReloadStopState(bool is_loading, bool force) { 238 [controller_ setIsLoading:is_loading force:force]; 239 } 240 241 void BrowserWindowCocoa::UpdateToolbar(TabContentsWrapper* contents, 242 bool should_restore_state) { 243 [controller_ updateToolbarWithContents:contents->tab_contents() 244 shouldRestoreState:should_restore_state ? YES : NO]; 245 } 246 247 void BrowserWindowCocoa::FocusToolbar() { 248 // Not needed on the Mac. 249 } 250 251 void BrowserWindowCocoa::FocusAppMenu() { 252 // Chrome uses the standard Mac OS X menu bar, so this isn't needed. 253 } 254 255 void BrowserWindowCocoa::RotatePaneFocus(bool forwards) { 256 // Not needed on the Mac. 257 } 258 259 void BrowserWindowCocoa::FocusBookmarksToolbar() { 260 // Not needed on the Mac. 261 } 262 263 void BrowserWindowCocoa::FocusChromeOSStatus() { 264 // Not needed on the Mac. 265 } 266 267 bool BrowserWindowCocoa::IsBookmarkBarVisible() const { 268 return (browser_->profile()->GetPrefs()->GetBoolean( 269 prefs::kShowBookmarkBar) && 270 browser_->profile()->GetPrefs()->GetBoolean( 271 prefs::kEnableBookmarkBar)); 272 } 273 274 bool BrowserWindowCocoa::IsBookmarkBarAnimating() const { 275 return [controller_ isBookmarkBarAnimating]; 276 } 277 278 bool BrowserWindowCocoa::IsTabStripEditable() const { 279 return ![controller_ isDragSessionActive]; 280 } 281 282 bool 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. 291 void BrowserWindowCocoa::ToggleBookmarkBar() { 292 bookmark_utils::ToggleWhenVisible(browser_->profile()); 293 } 294 295 void BrowserWindowCocoa::AddFindBar( 296 FindBarCocoaController* find_bar_cocoa_controller) { 297 return [controller_ addFindBar:find_bar_cocoa_controller]; 298 } 299 300 void BrowserWindowCocoa::ShowAboutChromeDialog() { 301 // Go through AppController's implementation to bring up the branded panel. 302 [[NSApp delegate] orderFrontStandardAboutPanel:nil]; 303 } 304 305 void BrowserWindowCocoa::ShowUpdateChromeDialog() { 306 restart_browser::RequestRestart(window()); 307 } 308 309 void BrowserWindowCocoa::ShowTaskManager() { 310 TaskManagerMac::Show(false); 311 } 312 313 void BrowserWindowCocoa::ShowBackgroundPages() { 314 TaskManagerMac::Show(true); 315 } 316 317 void BrowserWindowCocoa::ShowBookmarkBubble(const GURL& url, 318 bool already_bookmarked) { 319 [controller_ showBookmarkBubbleForURL:url 320 alreadyBookmarked:(already_bookmarked ? YES : NO)]; 321 } 322 323 bool BrowserWindowCocoa::IsDownloadShelfVisible() const { 324 return [controller_ isDownloadShelfVisible] != NO; 325 } 326 327 DownloadShelf* BrowserWindowCocoa::GetDownloadShelf() { 328 DownloadShelfController* shelfController = [controller_ downloadShelf]; 329 return [shelfController bridge]; 330 } 331 332 void BrowserWindowCocoa::ShowRepostFormWarningDialog( 333 TabContents* tab_contents) { 334 RepostFormWarningMac::Create(GetNativeHandle(), tab_contents); 335 } 336 337 void BrowserWindowCocoa::ShowCollectedCookiesDialog(TabContents* tab_contents) { 338 // Deletes itself on close. 339 new CollectedCookiesMac(GetNativeHandle(), tab_contents); 340 } 341 342 void 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:]. 348 void 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 358 void BrowserWindowCocoa::ShowHTMLDialog(HtmlDialogUIDelegate* delegate, 359 gfx::NativeWindow parent_window) { 360 [HtmlDialogWindowController showHtmlDialog:delegate 361 profile:browser_->profile()]; 362 } 363 364 void BrowserWindowCocoa::UserChangedTheme() { 365 [controller_ userChangedTheme]; 366 } 367 368 int BrowserWindowCocoa::GetExtraRenderViewHeight() const { 369 // Currently this is only used on linux. 370 return 0; 371 } 372 373 void BrowserWindowCocoa::TabContentsFocused(TabContents* tab_contents) { 374 NOTIMPLEMENTED(); 375 } 376 377 void 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 384 void BrowserWindowCocoa::ShowAppMenu() { 385 // No-op. Mac doesn't support showing the menus via alt keys. 386 } 387 388 bool 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 407 void 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 444 int 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 487 bool 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 516 void BrowserWindowCocoa::ShowCreateWebAppShortcutsDialog( 517 TabContentsWrapper* tab_contents) { 518 NOTIMPLEMENTED(); 519 } 520 521 void BrowserWindowCocoa::ShowCreateChromeAppShortcutsDialog( 522 Profile* profile, const Extension* app) { 523 NOTIMPLEMENTED(); 524 } 525 526 void BrowserWindowCocoa::Cut() { 527 [NSApp sendAction:@selector(cut:) to:nil from:nil]; 528 } 529 530 void BrowserWindowCocoa::Copy() { 531 [NSApp sendAction:@selector(copy:) to:nil from:nil]; 532 } 533 534 void BrowserWindowCocoa::Paste() { 535 [NSApp sendAction:@selector(paste:) to:nil from:nil]; 536 } 537 538 void BrowserWindowCocoa::ToggleTabStripMode() { 539 [controller_ toggleTabStripDisplayMode]; 540 } 541 542 void BrowserWindowCocoa::OpenTabpose() { 543 [controller_ openTabpose]; 544 } 545 546 void BrowserWindowCocoa::PrepareForInstant() { 547 // TODO: implement fade as done on windows. 548 } 549 550 void BrowserWindowCocoa::ShowInstant(TabContentsWrapper* preview) { 551 [controller_ showInstant:preview->tab_contents()]; 552 } 553 554 void BrowserWindowCocoa::HideInstant(bool instant_is_active) { 555 [controller_ hideInstant]; 556 557 // TODO: add support for |instant_is_active|. 558 } 559 560 gfx::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 570 void 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 589 void 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 596 NSWindow* BrowserWindowCocoa::window() const { 597 return [controller_ window]; 598 } 599 600 void BrowserWindowCocoa::UpdateSidebarForContents(TabContents* tab_contents) { 601 if (tab_contents == browser_->GetSelectedTabContents()) { 602 [controller_ updateSidebarForContents:tab_contents]; 603 } 604 } 605