1 // Copyright (c) 2013 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.
5 #include "chrome/browser/ui/cocoa/panels/panel_stack_window_cocoa.h"
7 #include "base/logging.h"
8 #include "base/strings/sys_string_conversions.h"
9 #import "chrome/browser/ui/cocoa/panels/panel_cocoa.h"
10 #import "chrome/browser/ui/cocoa/panels/panel_utils_cocoa.h"
11 #include "chrome/browser/ui/panels/panel.h"
12 #include "chrome/browser/ui/panels/panel_manager.h"
13 #include "chrome/browser/ui/panels/stacked_panel_collection.h"
14 #include "ui/base/cocoa/window_size_constants.h"
15 #include "ui/gfx/canvas.h"
16 #include "ui/gfx/image/image.h"
17 #include "ui/gfx/image/image_skia.h"
18 #include "ui/gfx/image/image_skia_rep.h"
19 #include "ui/gfx/vector2d.h"
20 #include "ui/snapshot/snapshot.h"
22 // The delegate class to receive the notification from NSViewAnimation.
23 @interface BatchBoundsAnimationDelegate : NSObject<NSAnimationDelegate> {
25 PanelStackWindowCocoa* window_; // Weak pointer.
28 // Called when NSViewAnimation finishes the animation.
29 - (void)animationDidEnd:(NSAnimation*)animation;
32 @implementation BatchBoundsAnimationDelegate
34 - (id)initWithWindow:(PanelStackWindowCocoa*)window {
35 if ((self = [super init]))
40 - (void)animationDidEnd:(NSAnimation*)animation {
41 window_->BoundsUpdateAnimationEnded();
47 NativePanelStackWindow* NativePanelStackWindow::Create(
48 NativePanelStackWindowDelegate* delegate) {
49 return new PanelStackWindowCocoa(delegate);
52 PanelStackWindowCocoa::PanelStackWindowCocoa(
53 NativePanelStackWindowDelegate* delegate)
54 : delegate_(delegate),
55 attention_request_id_(0),
56 bounds_updates_started_(false),
57 animate_bounds_updates_(false),
58 bounds_animation_(nil) {
60 bounds_animation_delegate_.reset(
61 [[BatchBoundsAnimationDelegate alloc] initWithWindow:this]);
64 PanelStackWindowCocoa::~PanelStackWindowCocoa() {
67 void PanelStackWindowCocoa::Close() {
68 TerminateBoundsAnimation();
72 void PanelStackWindowCocoa::AddPanel(Panel* panel) {
73 panels_.push_back(panel);
75 EnsureWindowCreated();
77 // Make the stack window own the panel window such that all panels window
78 // could be moved simulatenously when the stack window is moved.
79 [window_ addChildWindow:panel->GetNativeWindow() ordered:NSWindowAbove];
81 UpdateStackWindowBounds();
84 void PanelStackWindowCocoa::RemovePanel(Panel* panel) {
85 panels_.remove(panel);
87 // If the native panel is closed, the native window should already be gone.
88 if (!static_cast<PanelCocoa*>(panel->native_panel())->IsClosed())
89 [window_ removeChildWindow:panel->GetNativeWindow()];
91 UpdateStackWindowBounds();
94 void PanelStackWindowCocoa::MergeWith(NativePanelStackWindow* another) {
95 PanelStackWindowCocoa* another_stack =
96 static_cast<PanelStackWindowCocoa*>(another);
98 for (Panels::const_iterator iter = another_stack->panels_.begin();
99 iter != another_stack->panels_.end(); ++iter) {
100 Panel* panel = *iter;
101 panels_.push_back(panel);
103 // Change the panel window owner.
104 NSWindow* panel_window = panel->GetNativeWindow();
105 [another_stack->window_ removeChildWindow:panel_window];
106 [window_ addChildWindow:panel_window ordered:NSWindowAbove];
108 another_stack->panels_.clear();
110 UpdateStackWindowBounds();
113 bool PanelStackWindowCocoa::IsEmpty() const {
114 return panels_.empty();
117 bool PanelStackWindowCocoa::HasPanel(Panel* panel) const {
118 return std::find(panels_.begin(), panels_.end(), panel) != panels_.end();
121 void PanelStackWindowCocoa::MovePanelsBy(const gfx::Vector2d& delta) {
122 // Moving the background stack window will cause all foreground panels window
123 // being moved simulatenously.
124 gfx::Rect enclosing_bounds = GetStackWindowBounds();
125 enclosing_bounds.Offset(delta);
126 NSRect frame = cocoa_utils::ConvertRectToCocoaCoordinates(enclosing_bounds);
127 [window_ setFrame:frame display:NO];
129 // We also need to update the panel bounds.
130 for (Panels::const_iterator iter = panels_.begin();
131 iter != panels_.end(); ++iter) {
132 Panel* panel = *iter;
133 gfx::Rect bounds = panel->GetBounds();
134 bounds.Offset(delta);
135 panel->SetPanelBoundsInstantly(bounds);
139 void PanelStackWindowCocoa::BeginBatchUpdatePanelBounds(bool animate) {
140 // If the batch animation is still in progress, continue the animation
141 // with the new target bounds even we want to update the bounds instantly
143 if (!bounds_updates_started_) {
144 animate_bounds_updates_ = animate;
145 bounds_updates_started_ = true;
149 void PanelStackWindowCocoa::AddPanelBoundsForBatchUpdate(
150 Panel* panel, const gfx::Rect& new_bounds) {
151 DCHECK(bounds_updates_started_);
153 // No need to track it if no change is needed.
154 if (panel->GetBounds() == new_bounds)
157 // Old bounds are stored as the map value.
158 bounds_updates_[panel] = panel->GetBounds();
160 // New bounds are directly applied to the value stored in native panel
162 static_cast<PanelCocoa*>(panel->native_panel())->set_cached_bounds_directly(
166 void PanelStackWindowCocoa::EndBatchUpdatePanelBounds() {
167 DCHECK(bounds_updates_started_);
169 // No need to proceed with the animation when the bounds update list is
170 // empty or animation was not requested.
171 if (bounds_updates_.empty() || !animate_bounds_updates_) {
172 // Set the bounds directly when the update list is not empty.
173 if (!bounds_updates_.empty()) {
174 for (BoundsUpdates::const_iterator iter = bounds_updates_.begin();
175 iter != bounds_updates_.end(); ++iter) {
176 Panel* panel = iter->first;
178 cocoa_utils::ConvertRectToCocoaCoordinates(panel->GetBounds());
179 [panel->GetNativeWindow() setFrame:frame display:YES animate:NO];
181 bounds_updates_.clear();
182 UpdateStackWindowBounds();
185 bounds_updates_started_ = false;
186 delegate_->PanelBoundsBatchUpdateCompleted();
190 // Terminate previous animation, if it is still playing.
191 TerminateBoundsAnimation();
193 // Find out if we need the animation for each panel. If the batch updates
194 // consist of only moving all panels by delta offset, moving the background
195 // window would be enough.
197 // If all the panels move and don't resize, just animate the underlying
198 // parent window. Otherwise, animate each individual panel.
199 bool need_to_animate_individual_panels = false;
200 if (bounds_updates_.size() == panels_.size()) {
202 for (BoundsUpdates::const_iterator iter = bounds_updates_.begin();
203 iter != bounds_updates_.end(); ++iter) {
204 gfx::Rect old_bounds = iter->second;
205 gfx::Rect new_bounds = iter->first->GetBounds();
207 // Size should not be changed.
208 if (old_bounds.width() != new_bounds.width() ||
209 old_bounds.height() != new_bounds.height()) {
210 need_to_animate_individual_panels = true;
214 // Origin offset should be same.
215 if (iter == bounds_updates_.begin()) {
216 delta = new_bounds.origin() - old_bounds.origin();
217 } else if (!(delta == new_bounds.origin() - old_bounds.origin())) {
218 need_to_animate_individual_panels = true;
223 need_to_animate_individual_panels = true;
226 int num_of_animations = 1;
227 if (need_to_animate_individual_panels)
228 num_of_animations += bounds_updates_.size();
229 base::scoped_nsobject<NSMutableArray> animations(
230 [[NSMutableArray alloc] initWithCapacity:num_of_animations]);
232 // Add the animation for each panel in the update list.
233 if (need_to_animate_individual_panels) {
234 for (BoundsUpdates::const_iterator iter = bounds_updates_.begin();
235 iter != bounds_updates_.end(); ++iter) {
236 Panel* panel = iter->first;
238 cocoa_utils::ConvertRectToCocoaCoordinates(panel->GetBounds());
239 NSDictionary* animation = [NSDictionary dictionaryWithObjectsAndKeys:
240 panel->GetNativeWindow(), NSViewAnimationTargetKey,
241 [NSValue valueWithRect:panel_frame], NSViewAnimationEndFrameKey,
243 [animations addObject:animation];
247 // Compute the final bounds that enclose all panels after the animation.
248 gfx::Rect enclosing_bounds;
249 for (Panels::const_iterator iter = panels_.begin();
250 iter != panels_.end(); ++iter) {
251 gfx::Rect target_bounds = (*iter)->GetBounds();
252 enclosing_bounds = UnionRects(enclosing_bounds, target_bounds);
255 // Add the animation for the background stack window.
256 NSRect enclosing_frame =
257 cocoa_utils::ConvertRectToCocoaCoordinates(enclosing_bounds);
258 NSDictionary* stack_animation = [NSDictionary dictionaryWithObjectsAndKeys:
259 window_.get(), NSViewAnimationTargetKey,
260 [NSValue valueWithRect:enclosing_frame], NSViewAnimationEndFrameKey,
262 [animations addObject:stack_animation];
264 // Start all the animations.
265 // |bounds_animation_| is released when the animation ends.
267 [[NSViewAnimation alloc] initWithViewAnimations:animations];
268 [bounds_animation_ setDelegate:bounds_animation_delegate_.get()];
269 [bounds_animation_ setDuration:PanelManager::AdjustTimeInterval(0.18)];
270 [bounds_animation_ setFrameRate:0.0];
271 [bounds_animation_ setAnimationBlockingMode: NSAnimationNonblocking];
272 [bounds_animation_ startAnimation];
275 bool PanelStackWindowCocoa::IsAnimatingPanelBounds() const {
276 return bounds_updates_started_ && animate_bounds_updates_;
279 void PanelStackWindowCocoa::BoundsUpdateAnimationEnded() {
280 bounds_updates_started_ = false;
282 for (BoundsUpdates::const_iterator iter = bounds_updates_.begin();
283 iter != bounds_updates_.end(); ++iter) {
284 Panel* panel = iter->first;
285 panel->manager()->OnPanelAnimationEnded(panel);
287 bounds_updates_.clear();
289 delegate_->PanelBoundsBatchUpdateCompleted();
292 void PanelStackWindowCocoa::Minimize() {
293 // Provide the custom miniwindow image since there is nothing painted for
294 // the background stack window.
295 gfx::Size stack_window_size = GetStackWindowBounds().size();
296 gfx::Canvas canvas(stack_window_size, 1.0f, true);
298 Panels::const_iterator iter = panels_.begin();
299 for (; iter != panels_.end(); ++iter) {
300 Panel* panel = *iter;
301 gfx::Rect snapshot_bounds = gfx::Rect(panel->GetBounds().size());
302 std::vector<unsigned char> png;
303 if (!ui::GrabWindowSnapshot(panel->GetNativeWindow(),
307 gfx::Image snapshot_image = gfx::Image::CreateFrom1xPNGBytes(
308 &(png[0]), png.size());
309 canvas.DrawImageInt(snapshot_image.AsImageSkia(), 0, y);
310 y += snapshot_bounds.height();
312 if (iter == panels_.end()) {
313 gfx::Image image(gfx::ImageSkia(canvas.ExtractImageRep()));
314 [window_ setMiniwindowImage:image.AsNSImage()];
317 [window_ miniaturize:nil];
320 bool PanelStackWindowCocoa::IsMinimized() const {
321 return [window_ isMiniaturized];
324 void PanelStackWindowCocoa::DrawSystemAttention(bool draw_attention) {
325 BOOL is_drawing_attention = attention_request_id_ != 0;
326 if (draw_attention == is_drawing_attention)
329 if (draw_attention) {
330 attention_request_id_ = [NSApp requestUserAttention:NSCriticalRequest];
332 [NSApp cancelUserAttentionRequest:attention_request_id_];
333 attention_request_id_ = 0;
337 void PanelStackWindowCocoa::OnPanelActivated(Panel* panel) {
341 void PanelStackWindowCocoa::TerminateBoundsAnimation() {
342 if (!bounds_animation_)
344 [bounds_animation_ stopAnimation];
345 [bounds_animation_ setDelegate:nil];
346 [bounds_animation_ release];
347 bounds_animation_ = nil;
350 gfx::Rect PanelStackWindowCocoa::GetStackWindowBounds() const {
351 gfx::Rect enclosing_bounds;
352 for (Panels::const_iterator iter = panels_.begin();
353 iter != panels_.end(); ++iter) {
354 Panel* panel = *iter;
355 enclosing_bounds = UnionRects(enclosing_bounds, panel->GetBounds());
357 return enclosing_bounds;
360 void PanelStackWindowCocoa::UpdateStackWindowBounds() {
361 NSRect enclosing_bounds =
362 cocoa_utils::ConvertRectToCocoaCoordinates(GetStackWindowBounds());
363 [window_ setFrame:enclosing_bounds display:NO];
366 void PanelStackWindowCocoa::EnsureWindowCreated() {
371 [[NSWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater
372 styleMask:NSBorderlessWindowMask
373 backing:NSBackingStoreBuffered
375 [window_ setBackgroundColor:[NSColor clearColor]];
376 [window_ setHasShadow:YES];
377 [window_ setLevel:NSNormalWindowLevel];
378 [window_ orderFront:nil];
379 [window_ setTitle:base::SysUTF16ToNSString(delegate_->GetTitle())];