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 #include "chrome/browser/ui/cocoa/panels/panel_cocoa.h" 6 7 #include "base/logging.h" 8 #import "chrome/browser/ui/cocoa/chrome_event_processing_window.h" 9 #import "chrome/browser/ui/cocoa/panels/panel_titlebar_view_cocoa.h" 10 #import "chrome/browser/ui/cocoa/panels/panel_utils_cocoa.h" 11 #import "chrome/browser/ui/cocoa/panels/panel_window_controller_cocoa.h" 12 #include "chrome/browser/ui/panels/panel.h" 13 #include "chrome/browser/ui/panels/stacked_panel_collection.h" 14 #include "content/public/browser/native_web_keyboard_event.h" 15 16 using content::NativeWebKeyboardEvent; 17 using content::WebContents; 18 19 namespace { 20 21 // Use this instead of 0 for minimum size of a window when doing opening and 22 // closing animations, since OSX window manager does not like 0-sized windows 23 // (according to avi@). 24 const int kMinimumWindowSize = 1; 25 26 } // namespace 27 28 // This creates a shim window class, which in turn creates a Cocoa window 29 // controller which in turn creates actual NSWindow by loading a nib. 30 // Overall chain of ownership is: 31 // PanelWindowControllerCocoa -> PanelCocoa -> Panel. 32 // static 33 NativePanel* Panel::CreateNativePanel(Panel* panel, 34 const gfx::Rect& bounds, 35 bool always_on_top) { 36 return new PanelCocoa(panel, bounds, always_on_top); 37 } 38 39 PanelCocoa::PanelCocoa(Panel* panel, 40 const gfx::Rect& bounds, 41 bool always_on_top) 42 : panel_(panel), 43 bounds_(bounds), 44 always_on_top_(always_on_top), 45 is_shown_(false), 46 attention_request_id_(0), 47 corner_style_(panel::ALL_ROUNDED) { 48 controller_ = [[PanelWindowControllerCocoa alloc] initWithPanel:this]; 49 } 50 51 PanelCocoa::~PanelCocoa() { 52 } 53 54 bool PanelCocoa::IsClosed() const { 55 return !controller_; 56 } 57 58 void PanelCocoa::ShowPanel() { 59 ShowPanelInactive(); 60 ActivatePanel(); 61 62 // |-makeKeyAndOrderFront:| won't send |-windowDidBecomeKey:| until we 63 // return to the runloop. This causes extension tests that wait for the 64 // active status change notification to fail, so we send an active status 65 // notification here. 66 panel_->OnActiveStateChanged(true); 67 } 68 69 void PanelCocoa::ShowPanelInactive() { 70 if (IsClosed()) 71 return; 72 73 // This method may be called several times, meaning 'ensure it's shown'. 74 // Animations don't look good when repeated - hence this flag is needed. 75 if (is_shown_) { 76 return; 77 } 78 // A call to SetPanelBounds() before showing it is required to set 79 // the panel frame properly. 80 SetPanelBoundsInstantly(bounds_); 81 is_shown_ = true; 82 83 NSRect finalFrame = cocoa_utils::ConvertRectToCocoaCoordinates(bounds_); 84 [controller_ revealAnimatedWithFrame:finalFrame]; 85 } 86 87 gfx::Rect PanelCocoa::GetPanelBounds() const { 88 return bounds_; 89 } 90 91 // |bounds| is the platform-independent screen coordinates, with (0,0) at 92 // top-left of the primary screen. 93 void PanelCocoa::SetPanelBounds(const gfx::Rect& bounds) { 94 setBoundsInternal(bounds, true); 95 } 96 97 void PanelCocoa::SetPanelBoundsInstantly(const gfx::Rect& bounds) { 98 setBoundsInternal(bounds, false); 99 } 100 101 void PanelCocoa::setBoundsInternal(const gfx::Rect& bounds, bool animate) { 102 // We will call SetPanelBoundsInstantly() once before showing the panel 103 // and it should set the panel frame correctly. 104 if (bounds_ == bounds && is_shown_) 105 return; 106 107 bounds_ = bounds; 108 109 // Safely ignore calls to animate bounds before the panel is shown to 110 // prevent the window from loading prematurely. 111 if (animate && !is_shown_) 112 return; 113 114 NSRect frame = cocoa_utils::ConvertRectToCocoaCoordinates(bounds); 115 [controller_ setPanelFrame:frame animate:animate]; 116 } 117 118 void PanelCocoa::ClosePanel() { 119 if (IsClosed()) 120 return; 121 122 NSWindow* window = [controller_ window]; 123 // performClose: contains a nested message loop which can cause reentrancy 124 // if the browser is terminating and closing all the windows. 125 // Use this version that corresponds to protocol of performClose: but does not 126 // spin a nested loop. 127 // TODO(dimich): refactor similar method from BWC and reuse here. 128 if ([controller_ windowShouldClose:window]) { 129 // Make sure that the panel window is not associated with the underlying 130 // stack window because otherwise hiding the panel window could cause all 131 // other panel windows in the same stack to disappear. 132 NSWindow* stackWindow = [window parentWindow]; 133 if (stackWindow) 134 [stackWindow removeChildWindow:window]; 135 136 [window orderOut:nil]; 137 [window close]; 138 } 139 } 140 141 void PanelCocoa::ActivatePanel() { 142 if (!is_shown_) 143 return; 144 145 [controller_ activate]; 146 } 147 148 void PanelCocoa::DeactivatePanel() { 149 [controller_ deactivate]; 150 } 151 152 bool PanelCocoa::IsPanelActive() const { 153 // TODO(dcheng): It seems like a lot of these methods can be called before 154 // our NSWindow is created. Do we really need to check in every one of these 155 // methods if the NSWindow is created, or is there a better way to 156 // gracefully handle this? 157 if (!is_shown_) 158 return false; 159 return [[controller_ window] isMainWindow]; 160 } 161 162 void PanelCocoa::PreventActivationByOS(bool prevent_activation) { 163 [controller_ preventBecomingKeyWindow:prevent_activation]; 164 return; 165 } 166 167 gfx::NativeWindow PanelCocoa::GetNativePanelWindow() { 168 return [controller_ window]; 169 } 170 171 void PanelCocoa::UpdatePanelTitleBar() { 172 if (!is_shown_) 173 return; 174 [controller_ updateTitleBar]; 175 } 176 177 void PanelCocoa::UpdatePanelLoadingAnimations(bool should_animate) { 178 [controller_ updateThrobber:should_animate]; 179 } 180 181 void PanelCocoa::PanelWebContentsFocused(content::WebContents* contents) { 182 // Nothing to do. 183 } 184 185 void PanelCocoa::PanelCut() { 186 // Nothing to do since we do not have panel-specific system menu on Mac. 187 } 188 189 void PanelCocoa::PanelCopy() { 190 // Nothing to do since we do not have panel-specific system menu on Mac. 191 } 192 193 void PanelCocoa::PanelPaste() { 194 // Nothing to do since we do not have panel-specific system menu on Mac. 195 } 196 197 void PanelCocoa::DrawAttention(bool draw_attention) { 198 DCHECK((panel_->attention_mode() & Panel::USE_PANEL_ATTENTION) != 0); 199 200 PanelTitlebarViewCocoa* titlebar = [controller_ titlebarView]; 201 if (draw_attention) 202 [titlebar drawAttention]; 203 else 204 [titlebar stopDrawingAttention]; 205 206 if ((panel_->attention_mode() & Panel::USE_SYSTEM_ATTENTION) != 0) { 207 if (draw_attention) { 208 DCHECK(!attention_request_id_); 209 attention_request_id_ = [NSApp requestUserAttention:NSCriticalRequest]; 210 } else { 211 [NSApp cancelUserAttentionRequest:attention_request_id_]; 212 attention_request_id_ = 0; 213 } 214 } 215 } 216 217 bool PanelCocoa::IsDrawingAttention() const { 218 PanelTitlebarViewCocoa* titlebar = [controller_ titlebarView]; 219 return [titlebar isDrawingAttention]; 220 } 221 222 void PanelCocoa::HandlePanelKeyboardEvent( 223 const NativeWebKeyboardEvent& event) { 224 if (event.skip_in_browser || event.type == NativeWebKeyboardEvent::Char) 225 return; 226 227 ChromeEventProcessingWindow* event_window = 228 static_cast<ChromeEventProcessingWindow*>([controller_ window]); 229 DCHECK([event_window isKindOfClass:[ChromeEventProcessingWindow class]]); 230 [event_window redispatchKeyEvent:event.os_event]; 231 } 232 233 void PanelCocoa::FullScreenModeChanged(bool is_full_screen) { 234 if (!is_shown_) { 235 // If the panel window is not shown due to that a Chrome tab window is in 236 // fullscreen mode when the panel is being created, we need to show the 237 // panel window now. In addition, its titlebar needs to be updated since it 238 // is not done at the panel creation time. 239 if (!is_full_screen) { 240 ShowPanelInactive(); 241 UpdatePanelTitleBar(); 242 } 243 244 // No need to proceed when the panel window was not shown previously. 245 // We either show the panel window or do not show it depending on current 246 // full screen state. 247 return; 248 } 249 [controller_ fullScreenModeChanged:is_full_screen]; 250 } 251 252 bool PanelCocoa::IsPanelAlwaysOnTop() const { 253 return always_on_top_; 254 } 255 256 void PanelCocoa::SetPanelAlwaysOnTop(bool on_top) { 257 if (always_on_top_ == on_top) 258 return; 259 always_on_top_ = on_top; 260 [controller_ updateWindowLevel]; 261 [controller_ updateWindowCollectionBehavior]; 262 } 263 264 void PanelCocoa::EnableResizeByMouse(bool enable) { 265 [controller_ enableResizeByMouse:enable]; 266 } 267 268 void PanelCocoa::UpdatePanelMinimizeRestoreButtonVisibility() { 269 [controller_ updateTitleBarMinimizeRestoreButtonVisibility]; 270 } 271 272 void PanelCocoa::SetWindowCornerStyle(panel::CornerStyle corner_style) { 273 corner_style_ = corner_style; 274 275 // TODO(dimich): investigate how to support it on Mac. 276 } 277 278 void PanelCocoa::MinimizePanelBySystem() { 279 [controller_ miniaturize]; 280 } 281 282 bool PanelCocoa::IsPanelMinimizedBySystem() const { 283 return [controller_ isMiniaturized]; 284 } 285 286 bool PanelCocoa::IsPanelShownOnActiveDesktop() const { 287 return [[controller_ window] isOnActiveSpace]; 288 } 289 290 void PanelCocoa::ShowShadow(bool show) { 291 [controller_ showShadow:show]; 292 } 293 294 void PanelCocoa::PanelExpansionStateChanging( 295 Panel::ExpansionState old_state, Panel::ExpansionState new_state) { 296 [controller_ updateWindowLevel:(new_state != Panel::EXPANDED)]; 297 } 298 299 void PanelCocoa::AttachWebContents(content::WebContents* contents) { 300 [controller_ webContentsInserted:contents]; 301 } 302 303 void PanelCocoa::DetachWebContents(content::WebContents* contents) { 304 [controller_ webContentsDetached:contents]; 305 } 306 307 gfx::Size PanelCocoa::WindowSizeFromContentSize( 308 const gfx::Size& content_size) const { 309 NSRect content = NSMakeRect(0, 0, 310 content_size.width(), content_size.height()); 311 NSRect frame = [controller_ frameRectForContentRect:content]; 312 return gfx::Size(NSWidth(frame), NSHeight(frame)); 313 } 314 315 gfx::Size PanelCocoa::ContentSizeFromWindowSize( 316 const gfx::Size& window_size) const { 317 NSRect frame = NSMakeRect(0, 0, window_size.width(), window_size.height()); 318 NSRect content = [controller_ contentRectForFrameRect:frame]; 319 return gfx::Size(NSWidth(content), NSHeight(content)); 320 } 321 322 int PanelCocoa::TitleOnlyHeight() const { 323 return [controller_ titlebarHeightInScreenCoordinates]; 324 } 325 326 Panel* PanelCocoa::panel() const { 327 return panel_.get(); 328 } 329 330 void PanelCocoa::DidCloseNativeWindow() { 331 DCHECK(!IsClosed()); 332 controller_ = NULL; 333 panel_->OnNativePanelClosed(); 334 } 335 336 // NativePanelTesting implementation. 337 class CocoaNativePanelTesting : public NativePanelTesting { 338 public: 339 CocoaNativePanelTesting(NativePanel* native_panel); 340 virtual ~CocoaNativePanelTesting() { } 341 // Overridden from NativePanelTesting 342 virtual void PressLeftMouseButtonTitlebar( 343 const gfx::Point& mouse_location, panel::ClickModifier modifier) OVERRIDE; 344 virtual void ReleaseMouseButtonTitlebar( 345 panel::ClickModifier modifier) OVERRIDE; 346 virtual void DragTitlebar(const gfx::Point& mouse_location) OVERRIDE; 347 virtual void CancelDragTitlebar() OVERRIDE; 348 virtual void FinishDragTitlebar() OVERRIDE; 349 virtual bool VerifyDrawingAttention() const OVERRIDE; 350 virtual bool VerifyActiveState(bool is_active) OVERRIDE; 351 virtual bool VerifyAppIcon() const OVERRIDE; 352 virtual bool VerifySystemMinimizeState() const OVERRIDE; 353 virtual bool IsWindowSizeKnown() const OVERRIDE; 354 virtual bool IsAnimatingBounds() const OVERRIDE; 355 virtual bool IsButtonVisible( 356 panel::TitlebarButtonType button_type) const OVERRIDE; 357 virtual panel::CornerStyle GetWindowCornerStyle() const OVERRIDE; 358 virtual bool EnsureApplicationRunOnForeground() OVERRIDE; 359 360 private: 361 PanelTitlebarViewCocoa* titlebar() const; 362 // Weak, assumed always to outlive this test API object. 363 PanelCocoa* native_panel_window_; 364 }; 365 366 NativePanelTesting* PanelCocoa::CreateNativePanelTesting() { 367 return new CocoaNativePanelTesting(this); 368 } 369 370 CocoaNativePanelTesting::CocoaNativePanelTesting(NativePanel* native_panel) 371 : native_panel_window_(static_cast<PanelCocoa*>(native_panel)) { 372 } 373 374 PanelTitlebarViewCocoa* CocoaNativePanelTesting::titlebar() const { 375 return [native_panel_window_->controller_ titlebarView]; 376 } 377 378 void CocoaNativePanelTesting::PressLeftMouseButtonTitlebar( 379 const gfx::Point& mouse_location, panel::ClickModifier modifier) { 380 // Convert from platform-indepedent screen coordinates to Cocoa's screen 381 // coordinates because PanelTitlebarViewCocoa method takes Cocoa's screen 382 // coordinates. 383 int modifierFlags = 384 (modifier == panel::APPLY_TO_ALL ? NSShiftKeyMask : 0); 385 [titlebar() pressLeftMouseButtonTitlebar: 386 cocoa_utils::ConvertPointToCocoaCoordinates(mouse_location) 387 modifiers:modifierFlags]; 388 } 389 390 void CocoaNativePanelTesting::ReleaseMouseButtonTitlebar( 391 panel::ClickModifier modifier) { 392 int modifierFlags = 393 (modifier == panel::APPLY_TO_ALL ? NSShiftKeyMask : 0); 394 [titlebar() releaseLeftMouseButtonTitlebar:modifierFlags]; 395 } 396 397 void CocoaNativePanelTesting::DragTitlebar(const gfx::Point& mouse_location) { 398 // Convert from platform-indepedent screen coordinates to Cocoa's screen 399 // coordinates because PanelTitlebarViewCocoa method takes Cocoa's screen 400 // coordinates. 401 [titlebar() dragTitlebar: 402 cocoa_utils::ConvertPointToCocoaCoordinates(mouse_location)]; 403 } 404 405 void CocoaNativePanelTesting::CancelDragTitlebar() { 406 [titlebar() cancelDragTitlebar]; 407 } 408 409 void CocoaNativePanelTesting::FinishDragTitlebar() { 410 [titlebar() finishDragTitlebar]; 411 } 412 413 bool CocoaNativePanelTesting::VerifyDrawingAttention() const { 414 return [titlebar() isDrawingAttention]; 415 } 416 417 bool CocoaNativePanelTesting::VerifyActiveState(bool is_active) { 418 // TODO(jianli): to be implemented. 419 return false; 420 } 421 422 bool CocoaNativePanelTesting::VerifyAppIcon() const { 423 // Nothing to do since panel does not show dock icon. 424 return true; 425 } 426 427 bool CocoaNativePanelTesting::VerifySystemMinimizeState() const { 428 // TODO(jianli): to be implemented. 429 return true; 430 } 431 432 bool CocoaNativePanelTesting::IsWindowSizeKnown() const { 433 return true; 434 } 435 436 bool CocoaNativePanelTesting::IsAnimatingBounds() const { 437 if ([native_panel_window_->controller_ isAnimatingBounds]) 438 return true; 439 StackedPanelCollection* stack = native_panel_window_->panel()->stack(); 440 if (!stack) 441 return false; 442 return stack->IsAnimatingPanelBounds(native_panel_window_->panel()); 443 } 444 445 bool CocoaNativePanelTesting::IsButtonVisible( 446 panel::TitlebarButtonType button_type) const { 447 switch (button_type) { 448 case panel::CLOSE_BUTTON: 449 return ![[titlebar() closeButton] isHidden]; 450 case panel::MINIMIZE_BUTTON: 451 return ![[titlebar() minimizeButton] isHidden]; 452 case panel::RESTORE_BUTTON: 453 return ![[titlebar() restoreButton] isHidden]; 454 default: 455 NOTREACHED(); 456 } 457 return false; 458 } 459 460 panel::CornerStyle CocoaNativePanelTesting::GetWindowCornerStyle() const { 461 return native_panel_window_->corner_style_; 462 } 463 464 bool CocoaNativePanelTesting::EnsureApplicationRunOnForeground() { 465 if ([NSApp isActive]) 466 return true; 467 [NSApp activateIgnoringOtherApps:YES]; 468 return [NSApp isActive]; 469 } 470