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