// Copyright (c) 2013 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. #include "chrome/browser/ui/cocoa/panels/panel_stack_window_cocoa.h" #include "base/logging.h" #include "base/strings/sys_string_conversions.h" #import "chrome/browser/ui/cocoa/panels/panel_cocoa.h" #import "chrome/browser/ui/cocoa/panels/panel_utils_cocoa.h" #include "chrome/browser/ui/panels/panel.h" #include "chrome/browser/ui/panels/panel_manager.h" #include "chrome/browser/ui/panels/stacked_panel_collection.h" #include "ui/base/cocoa/window_size_constants.h" #include "ui/gfx/canvas.h" #include "ui/gfx/geometry/vector2d.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_skia.h" #include "ui/gfx/image/image_skia_rep.h" #include "ui/snapshot/snapshot.h" // The delegate class to receive the notification from NSViewAnimation. @interface BatchBoundsAnimationDelegate : NSObject { @private PanelStackWindowCocoa* window_; // Weak pointer. } // Called when NSViewAnimation finishes the animation. - (void)animationDidEnd:(NSAnimation*)animation; @end @implementation BatchBoundsAnimationDelegate - (id)initWithWindow:(PanelStackWindowCocoa*)window { if ((self = [super init])) window_ = window; return self; } - (void)animationDidEnd:(NSAnimation*)animation { window_->BoundsUpdateAnimationEnded(); } @end // static NativePanelStackWindow* NativePanelStackWindow::Create( NativePanelStackWindowDelegate* delegate) { return new PanelStackWindowCocoa(delegate); } PanelStackWindowCocoa::PanelStackWindowCocoa( NativePanelStackWindowDelegate* delegate) : delegate_(delegate), attention_request_id_(0), bounds_updates_started_(false), animate_bounds_updates_(false), bounds_animation_(nil) { DCHECK(delegate); bounds_animation_delegate_.reset( [[BatchBoundsAnimationDelegate alloc] initWithWindow:this]); } PanelStackWindowCocoa::~PanelStackWindowCocoa() { } void PanelStackWindowCocoa::Close() { TerminateBoundsAnimation(); [window_ close]; } void PanelStackWindowCocoa::AddPanel(Panel* panel) { panels_.push_back(panel); EnsureWindowCreated(); // Make the stack window own the panel window such that all panels window // could be moved simulatenously when the stack window is moved. [window_ addChildWindow:panel->GetNativeWindow() ordered:NSWindowAbove]; UpdateStackWindowBounds(); } void PanelStackWindowCocoa::RemovePanel(Panel* panel) { if (IsAnimatingPanelBounds()) { // This panel is gone. We should not perform any update to it. bounds_updates_.erase(panel); } panels_.remove(panel); // If the native panel is closed, the native window should already be gone. if (!static_cast(panel->native_panel())->IsClosed()) [window_ removeChildWindow:panel->GetNativeWindow()]; UpdateStackWindowBounds(); } void PanelStackWindowCocoa::MergeWith(NativePanelStackWindow* another) { PanelStackWindowCocoa* another_stack = static_cast(another); for (Panels::const_iterator iter = another_stack->panels_.begin(); iter != another_stack->panels_.end(); ++iter) { Panel* panel = *iter; panels_.push_back(panel); // Change the panel window owner. NSWindow* panel_window = panel->GetNativeWindow(); [another_stack->window_ removeChildWindow:panel_window]; [window_ addChildWindow:panel_window ordered:NSWindowAbove]; } another_stack->panels_.clear(); UpdateStackWindowBounds(); } bool PanelStackWindowCocoa::IsEmpty() const { return panels_.empty(); } bool PanelStackWindowCocoa::HasPanel(Panel* panel) const { return std::find(panels_.begin(), panels_.end(), panel) != panels_.end(); } void PanelStackWindowCocoa::MovePanelsBy(const gfx::Vector2d& delta) { // Moving the background stack window will cause all foreground panels window // being moved simulatenously. gfx::Rect enclosing_bounds = GetStackWindowBounds(); enclosing_bounds.Offset(delta); NSRect frame = cocoa_utils::ConvertRectToCocoaCoordinates(enclosing_bounds); [window_ setFrame:frame display:NO]; // We also need to update the panel bounds. for (Panels::const_iterator iter = panels_.begin(); iter != panels_.end(); ++iter) { Panel* panel = *iter; gfx::Rect bounds = panel->GetBounds(); bounds.Offset(delta); panel->SetPanelBoundsInstantly(bounds); } } void PanelStackWindowCocoa::BeginBatchUpdatePanelBounds(bool animate) { // If the batch animation is still in progress, continue the animation // with the new target bounds even we want to update the bounds instantly // this time. if (!bounds_updates_started_) { animate_bounds_updates_ = animate; bounds_updates_started_ = true; } } void PanelStackWindowCocoa::AddPanelBoundsForBatchUpdate( Panel* panel, const gfx::Rect& new_bounds) { DCHECK(bounds_updates_started_); // No need to track it if no change is needed. if (panel->GetBounds() == new_bounds) return; // Old bounds are stored as the map value. bounds_updates_[panel] = panel->GetBounds(); // New bounds are directly applied to the value stored in native panel // window. static_cast(panel->native_panel())->set_cached_bounds_directly( new_bounds); } void PanelStackWindowCocoa::EndBatchUpdatePanelBounds() { DCHECK(bounds_updates_started_); // No need to proceed with the animation when the bounds update list is // empty or animation was not requested. if (bounds_updates_.empty() || !animate_bounds_updates_) { // Set the bounds directly when the update list is not empty. if (!bounds_updates_.empty()) { for (BoundsUpdates::const_iterator iter = bounds_updates_.begin(); iter != bounds_updates_.end(); ++iter) { Panel* panel = iter->first; NSRect frame = cocoa_utils::ConvertRectToCocoaCoordinates(panel->GetBounds()); [panel->GetNativeWindow() setFrame:frame display:YES animate:NO]; } bounds_updates_.clear(); UpdateStackWindowBounds(); } bounds_updates_started_ = false; delegate_->PanelBoundsBatchUpdateCompleted(); return; } // Terminate previous animation, if it is still playing. TerminateBoundsAnimation(); // Find out if we need the animation for each panel. If the batch updates // consist of only moving all panels by delta offset, moving the background // window would be enough. // If all the panels move and don't resize, just animate the underlying // parent window. Otherwise, animate each individual panel. bool need_to_animate_individual_panels = false; if (bounds_updates_.size() == panels_.size()) { gfx::Vector2d delta; for (BoundsUpdates::const_iterator iter = bounds_updates_.begin(); iter != bounds_updates_.end(); ++iter) { gfx::Rect old_bounds = iter->second; gfx::Rect new_bounds = iter->first->GetBounds(); // Size should not be changed. if (old_bounds.width() != new_bounds.width() || old_bounds.height() != new_bounds.height()) { need_to_animate_individual_panels = true; break; } // Origin offset should be same. if (iter == bounds_updates_.begin()) { delta = new_bounds.origin() - old_bounds.origin(); } else if (!(delta == new_bounds.origin() - old_bounds.origin())) { need_to_animate_individual_panels = true; break; } } } else { need_to_animate_individual_panels = true; } int num_of_animations = 1; if (need_to_animate_individual_panels) num_of_animations += bounds_updates_.size(); base::scoped_nsobject animations( [[NSMutableArray alloc] initWithCapacity:num_of_animations]); // Add the animation for each panel in the update list. if (need_to_animate_individual_panels) { for (BoundsUpdates::const_iterator iter = bounds_updates_.begin(); iter != bounds_updates_.end(); ++iter) { Panel* panel = iter->first; NSRect panel_frame = cocoa_utils::ConvertRectToCocoaCoordinates(panel->GetBounds()); NSDictionary* animation = [NSDictionary dictionaryWithObjectsAndKeys: panel->GetNativeWindow(), NSViewAnimationTargetKey, [NSValue valueWithRect:panel_frame], NSViewAnimationEndFrameKey, nil]; [animations addObject:animation]; } } // Compute the final bounds that enclose all panels after the animation. gfx::Rect enclosing_bounds; for (Panels::const_iterator iter = panels_.begin(); iter != panels_.end(); ++iter) { gfx::Rect target_bounds = (*iter)->GetBounds(); enclosing_bounds = UnionRects(enclosing_bounds, target_bounds); } // Add the animation for the background stack window. NSRect enclosing_frame = cocoa_utils::ConvertRectToCocoaCoordinates(enclosing_bounds); NSDictionary* stack_animation = [NSDictionary dictionaryWithObjectsAndKeys: window_.get(), NSViewAnimationTargetKey, [NSValue valueWithRect:enclosing_frame], NSViewAnimationEndFrameKey, nil]; [animations addObject:stack_animation]; // Start all the animations. // |bounds_animation_| is released when the animation ends. bounds_animation_ = [[NSViewAnimation alloc] initWithViewAnimations:animations]; [bounds_animation_ setDelegate:bounds_animation_delegate_.get()]; [bounds_animation_ setDuration:PanelManager::AdjustTimeInterval(0.18)]; [bounds_animation_ setFrameRate:0.0]; [bounds_animation_ setAnimationBlockingMode: NSAnimationNonblocking]; [bounds_animation_ startAnimation]; } bool PanelStackWindowCocoa::IsAnimatingPanelBounds() const { return bounds_updates_started_ && animate_bounds_updates_; } void PanelStackWindowCocoa::BoundsUpdateAnimationEnded() { bounds_updates_started_ = false; for (BoundsUpdates::const_iterator iter = bounds_updates_.begin(); iter != bounds_updates_.end(); ++iter) { Panel* panel = iter->first; panel->manager()->OnPanelAnimationEnded(panel); } bounds_updates_.clear(); delegate_->PanelBoundsBatchUpdateCompleted(); } void PanelStackWindowCocoa::Minimize() { // Provide the custom miniwindow image since there is nothing painted for // the background stack window. gfx::Size stack_window_size = GetStackWindowBounds().size(); gfx::Canvas canvas(stack_window_size, 1.0f, true); int y = 0; Panels::const_iterator iter = panels_.begin(); for (; iter != panels_.end(); ++iter) { Panel* panel = *iter; gfx::Rect snapshot_bounds = gfx::Rect(panel->GetBounds().size()); std::vector png; if (!ui::GrabWindowSnapshot(panel->GetNativeWindow(), &png, snapshot_bounds)) break; gfx::Image snapshot_image = gfx::Image::CreateFrom1xPNGBytes( &(png[0]), png.size()); canvas.DrawImageInt(snapshot_image.AsImageSkia(), 0, y); y += snapshot_bounds.height(); } if (iter == panels_.end()) { gfx::Image image(gfx::ImageSkia(canvas.ExtractImageRep())); [window_ setMiniwindowImage:image.AsNSImage()]; } [window_ miniaturize:nil]; } bool PanelStackWindowCocoa::IsMinimized() const { return [window_ isMiniaturized]; } void PanelStackWindowCocoa::DrawSystemAttention(bool draw_attention) { BOOL is_drawing_attention = attention_request_id_ != 0; if (draw_attention == is_drawing_attention) return; if (draw_attention) { attention_request_id_ = [NSApp requestUserAttention:NSCriticalRequest]; } else { [NSApp cancelUserAttentionRequest:attention_request_id_]; attention_request_id_ = 0; } } void PanelStackWindowCocoa::OnPanelActivated(Panel* panel) { // Nothing to do. } void PanelStackWindowCocoa::TerminateBoundsAnimation() { if (!bounds_animation_) return; [bounds_animation_ stopAnimation]; [bounds_animation_ setDelegate:nil]; [bounds_animation_ release]; bounds_animation_ = nil; } gfx::Rect PanelStackWindowCocoa::GetStackWindowBounds() const { gfx::Rect enclosing_bounds; for (Panels::const_iterator iter = panels_.begin(); iter != panels_.end(); ++iter) { Panel* panel = *iter; enclosing_bounds = UnionRects(enclosing_bounds, panel->GetBounds()); } return enclosing_bounds; } void PanelStackWindowCocoa::UpdateStackWindowBounds() { NSRect enclosing_bounds = cocoa_utils::ConvertRectToCocoaCoordinates(GetStackWindowBounds()); [window_ setFrame:enclosing_bounds display:NO]; } void PanelStackWindowCocoa::EnsureWindowCreated() { if (window_) return; window_.reset( [[NSWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]); [window_ setBackgroundColor:[NSColor clearColor]]; [window_ setHasShadow:YES]; [window_ setLevel:NSNormalWindowLevel]; [window_ orderFront:nil]; [window_ setTitle:base::SysUTF16ToNSString(delegate_->GetTitle())]; }