panel_stack_view.cc revision 7dbb3d5cf0c15f500944d211057644d6a2f37371
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.
4
5#include "chrome/browser/ui/views/panels/panel_stack_view.h"
6
7#include "base/logging.h"
8#include "base/strings/utf_string_conversions.h"
9#include "chrome/browser/profiles/profile.h"
10#include "chrome/browser/ui/panels/panel.h"
11#include "chrome/browser/ui/panels/panel_manager.h"
12#include "chrome/browser/ui/panels/stacked_panel_collection.h"
13#include "chrome/browser/ui/views/panels/panel_view.h"
14#include "ui/base/animation/linear_animation.h"
15#include "ui/gfx/image/image_skia.h"
16#include "ui/gfx/rect.h"
17#include "ui/views/widget/widget.h"
18
19#if defined(OS_WIN)
20#include "base/win/windows_version.h"
21#include "chrome/browser/shell_integration.h"
22#include "ui/base/win/shell.h"
23#include "ui/views/win/hwnd_util.h"
24#endif
25
26namespace {
27// These values are experimental and subjective.
28const int kDefaultFramerateHz = 50;
29const int kSetBoundsAnimationMs = 180;
30}
31
32// static
33NativePanelStackWindow* NativePanelStackWindow::Create(
34    NativePanelStackWindowDelegate* delegate) {
35#if defined(OS_WIN)
36  return new PanelStackView(delegate);
37#else
38  NOTIMPLEMENTED();
39  return NULL;
40#endif
41}
42
43PanelStackView::PanelStackView(NativePanelStackWindowDelegate* delegate)
44    : delegate_(delegate),
45      window_(NULL),
46      is_closing_(false),
47      is_drawing_attention_(false),
48      animate_bounds_updates_(false),
49      bounds_updates_started_(false) {
50  DCHECK(delegate);
51  views::WidgetFocusManager::GetInstance()->AddFocusChangeListener(this);
52}
53
54PanelStackView::~PanelStackView() {
55#if defined(OS_WIN)
56  ui::HWNDSubclass::RemoveFilterFromAllTargets(this);
57#endif
58}
59
60void PanelStackView::Close() {
61  is_closing_ = true;
62  delegate_ = NULL;
63  if (bounds_animator_)
64    bounds_animator_.reset();
65  if (window_)
66    window_->Close();
67  views::WidgetFocusManager::GetInstance()->RemoveFocusChangeListener(this);
68}
69
70void PanelStackView::AddPanel(Panel* panel) {
71  panels_.push_back(panel);
72
73  EnsureWindowCreated();
74  MakeStackWindowOwnPanelWindow(panel, this);
75  UpdateStackWindowBounds();
76
77  window_->UpdateWindowTitle();
78  window_->UpdateWindowIcon();
79}
80
81void PanelStackView::RemovePanel(Panel* panel) {
82  panels_.remove(panel);
83
84  MakeStackWindowOwnPanelWindow(panel, NULL);
85  UpdateStackWindowBounds();
86}
87
88void PanelStackView::MergeWith(NativePanelStackWindow* another) {
89  PanelStackView* another_stack = static_cast<PanelStackView*>(another);
90
91  for (Panels::const_iterator iter = another_stack->panels_.begin();
92       iter != another_stack->panels_.end(); ++iter) {
93    Panel* panel = *iter;
94    panels_.push_back(panel);
95    MakeStackWindowOwnPanelWindow(panel, this);
96  }
97  another_stack->panels_.clear();
98
99  UpdateStackWindowBounds();
100}
101
102bool PanelStackView::IsEmpty() const {
103  return panels_.empty();
104}
105
106bool PanelStackView::HasPanel(Panel* panel) const {
107  return std::find(panels_.begin(), panels_.end(), panel) != panels_.end();
108}
109
110void PanelStackView::MovePanelsBy(const gfx::Vector2d& delta) {
111  BeginBatchUpdatePanelBounds(false);
112  for (Panels::const_iterator iter = panels_.begin();
113       iter != panels_.end(); ++iter) {
114    Panel* panel = *iter;
115    AddPanelBoundsForBatchUpdate(panel, panel->GetBounds() + delta);
116  }
117  EndBatchUpdatePanelBounds();
118}
119
120void PanelStackView::BeginBatchUpdatePanelBounds(bool animate) {
121  // If the batch animation is still in progress, continue the animation
122  // with the new target bounds even we want to update the bounds instantly
123  // this time.
124  if (!bounds_updates_started_) {
125    animate_bounds_updates_ = animate;
126    bounds_updates_started_ = true;
127  }
128}
129
130void PanelStackView::AddPanelBoundsForBatchUpdate(Panel* panel,
131                                                  const gfx::Rect& new_bounds) {
132  DCHECK(bounds_updates_started_);
133
134  // No need to track it if no change is needed.
135  if (panel->GetBounds() == new_bounds)
136    return;
137
138  // Old bounds are stored as the map value.
139  bounds_updates_[panel] = panel->GetBounds();
140
141  // New bounds are directly applied to the valued stored in native panel
142  // window.
143  static_cast<PanelView*>(panel->native_panel())->set_cached_bounds_directly(
144      new_bounds);
145}
146
147void PanelStackView::EndBatchUpdatePanelBounds() {
148  DCHECK(bounds_updates_started_);
149
150  if (bounds_updates_.empty() || !animate_bounds_updates_) {
151    if (!bounds_updates_.empty()) {
152      UpdatePanelsBounds();
153      bounds_updates_.clear();
154    }
155
156    bounds_updates_started_ = false;
157    NotifyBoundsUpdateCompleted();
158    return;
159  }
160
161  bounds_animator_.reset(new ui::LinearAnimation(
162      PanelManager::AdjustTimeInterval(kSetBoundsAnimationMs),
163      kDefaultFramerateHz,
164      this));
165  bounds_animator_->Start();
166}
167
168void PanelStackView::NotifyBoundsUpdateCompleted() {
169  delegate_->PanelBoundsBatchUpdateCompleted();
170
171#if defined(OS_WIN)
172  // Refresh the thumbnail each time when any bounds updates are done.
173  RefreshLivePreviewThumbnail();
174#endif
175}
176
177bool PanelStackView::IsAnimatingPanelBounds() const {
178  return bounds_updates_started_ && animate_bounds_updates_;
179}
180
181void PanelStackView::Minimize() {
182#if defined(OS_WIN)
183  // When the stack window is minimized by the system, its snapshot could not
184  // be obtained. We need to capture the snapshot before the minimization.
185  if (thumbnailer_)
186    thumbnailer_->CaptureSnapshot();
187#endif
188
189  window_->Minimize();
190}
191
192bool PanelStackView::IsMinimized() const {
193  return window_ ? window_->IsMinimized() : false;
194}
195
196void PanelStackView::DrawSystemAttention(bool draw_attention) {
197  // The underlying call of FlashFrame, FlashWindowEx, seems not to work
198  // correctly if it is called more than once consecutively.
199  if (draw_attention == is_drawing_attention_)
200    return;
201  is_drawing_attention_ = draw_attention;
202
203#if defined(OS_WIN)
204  // Refresh the thumbnail when a panel could change something for the
205  // attention.
206  RefreshLivePreviewThumbnail();
207
208  if (draw_attention) {
209    // The default implementation of Widget::FlashFrame only flashes 5 times.
210    // We need more than that.
211    FLASHWINFO fwi;
212    fwi.cbSize = sizeof(fwi);
213    fwi.hwnd = views::HWNDForWidget(window_);
214    fwi.dwFlags = FLASHW_ALL;
215    fwi.uCount = panel::kNumberOfTimesToFlashPanelForAttention;
216    fwi.dwTimeout = 0;
217    ::FlashWindowEx(&fwi);
218  } else {
219    // Calling FlashWindowEx with FLASHW_STOP flag does not always work.
220    // Occasionally the taskbar icon could still remain in the flashed state.
221    // To work around this problem, we recreate the underlying window.
222    views::Widget* old_window = window_;
223    window_ = CreateWindowWithBounds(GetStackWindowBounds());
224
225    // New background window should also be minimized if the old one is.
226    if (old_window->IsMinimized())
227      window_->Minimize();
228
229    // Make sure the new background window stays at the same z-order as the old
230    // one.
231    ::SetWindowPos(views::HWNDForWidget(window_),
232                   views::HWNDForWidget(old_window),
233                   0, 0, 0, 0,
234                   SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
235    for (Panels::const_iterator iter = panels_.begin();
236         iter != panels_.end(); ++iter) {
237      MakeStackWindowOwnPanelWindow(*iter, this);
238    }
239
240    // Serve the snapshot to the new backgroud window.
241    if (thumbnailer_.get())
242      thumbnailer_->ReplaceWindow(views::HWNDForWidget(window_));
243
244    window_->UpdateWindowTitle();
245    window_->UpdateWindowIcon();
246    old_window->Close();
247  }
248#else
249  window_->FlashFrame(draw_attention);
250#endif
251}
252
253void PanelStackView::OnPanelActivated(Panel* panel) {
254  // Nothing to do.
255}
256
257string16 PanelStackView::GetWindowTitle() const {
258  return delegate_->GetTitle();
259}
260
261gfx::ImageSkia PanelStackView::GetWindowAppIcon() {
262  if (panels_.empty())
263    return gfx::ImageSkia();
264
265  Panel* panel = panels_.front();
266  gfx::Image app_icon = panel->app_icon();
267  if (!app_icon.IsEmpty())
268    return *app_icon.ToImageSkia();
269
270  return gfx::ImageSkia();
271}
272
273gfx::ImageSkia PanelStackView::GetWindowIcon() {
274  return GetWindowAppIcon();
275}
276
277views::Widget* PanelStackView::GetWidget() {
278  return window_;
279}
280
281const views::Widget* PanelStackView::GetWidget() const {
282  return window_;
283}
284
285void PanelStackView::DeleteDelegate() {
286  // |window_| could be closed when it is regenerated in order to clear the
287  // taskbar icon flash state. We should only delete this instance when the
288  // window is really being closed.
289  if (is_closing_)
290    delete this;
291}
292
293void PanelStackView::OnWidgetDestroying(views::Widget* widget) {
294  if (widget == window_)
295    window_ = NULL;
296}
297
298void PanelStackView::OnNativeFocusChange(gfx::NativeView focused_before,
299                                         gfx::NativeView focused_now) {
300  // When the user selects the stacked panels via ALT-TAB or WIN-TAB, the
301  // background stack window, instead of the foreground panel window, receives
302  // WM_SETFOCUS message. To deal with this, we listen to the focus change event
303  // and activate the most recently active panel.
304  // Note that OnNativeFocusChange might be called when window_ has not be
305  // created yet.
306#if defined(OS_WIN)
307  if (!panels_.empty() && window_ && focused_now == window_->GetNativeView()) {
308    Panel* panel_to_focus =
309        panels_.front()->stack()->most_recently_active_panel();
310    if (panel_to_focus)
311      panel_to_focus->Activate();
312  }
313#endif
314}
315
316void PanelStackView::AnimationEnded(const ui::Animation* animation) {
317  bounds_updates_started_ = false;
318
319  PanelManager* panel_manager = PanelManager::GetInstance();
320  for (BoundsUpdates::const_iterator iter = bounds_updates_.begin();
321       iter != bounds_updates_.end(); ++iter) {
322    panel_manager->OnPanelAnimationEnded(iter->first);
323  }
324  bounds_updates_.clear();
325
326  NotifyBoundsUpdateCompleted();
327}
328
329void PanelStackView::AnimationProgressed(const ui::Animation* animation) {
330  UpdatePanelsBounds();
331}
332
333void PanelStackView::UpdatePanelsBounds() {
334#if defined(OS_WIN)
335  // Add an extra count for the background stack window.
336  HDWP defer_update = ::BeginDeferWindowPos(bounds_updates_.size() + 1);
337#endif
338
339  // Update the bounds for each panel in the update list.
340  gfx::Rect enclosing_bounds;
341  for (BoundsUpdates::const_iterator iter = bounds_updates_.begin();
342       iter != bounds_updates_.end(); ++iter) {
343    Panel* panel = iter->first;
344    gfx::Rect target_bounds = panel->GetBounds();
345    gfx::Rect current_bounds;
346    if (bounds_animator_ && bounds_animator_->is_animating()) {
347      current_bounds = bounds_animator_->CurrentValueBetween(
348          iter->second, target_bounds);
349    } else {
350      current_bounds = target_bounds;
351    }
352
353    PanelView* panel_view = static_cast<PanelView*>(panel->native_panel());
354#if defined(OS_WIN)
355    DeferUpdateNativeWindowBounds(defer_update,
356                                  panel_view->window(),
357                                  current_bounds);
358#else
359    panel_view->SetPanelBoundsInstantly(current_bounds);
360#endif
361
362    enclosing_bounds = UnionRects(enclosing_bounds, current_bounds);
363  }
364
365  // Compute the stack window bounds that enclose those panels that are not
366  // in the batch update list.
367  for (Panels::const_iterator iter = panels_.begin();
368       iter != panels_.end(); ++iter) {
369    Panel* panel = *iter;
370    if (bounds_updates_.find(panel) == bounds_updates_.end())
371      enclosing_bounds = UnionRects(enclosing_bounds, panel->GetBounds());
372  }
373
374  // Update the bounds of the background stack window.
375#if defined(OS_WIN)
376  DeferUpdateNativeWindowBounds(defer_update, window_, enclosing_bounds);
377#else
378  window_->SetBounds(enclosing_bounds);
379#endif
380
381#if defined(OS_WIN)
382  ::EndDeferWindowPos(defer_update);
383#endif
384}
385
386gfx::Rect PanelStackView::GetStackWindowBounds() const {
387  gfx::Rect enclosing_bounds;
388  for (Panels::const_iterator iter = panels_.begin();
389       iter != panels_.end(); ++iter) {
390    Panel* panel = *iter;
391    enclosing_bounds = UnionRects(enclosing_bounds, panel->GetBounds());
392  }
393  return enclosing_bounds;
394}
395
396void PanelStackView::UpdateStackWindowBounds() {
397  window_->SetBounds(GetStackWindowBounds());
398
399#if defined(OS_WIN)
400  // Refresh the thumbnail each time whne the stack window is changed, due to
401  // adding or removing a panel.
402  RefreshLivePreviewThumbnail();
403#endif
404}
405
406// static
407void PanelStackView::MakeStackWindowOwnPanelWindow(
408    Panel* panel, PanelStackView* stack_window) {
409#if defined(OS_WIN)
410  // The panel widget window might already be gone when a panel is closed.
411  views::Widget* panel_window =
412      static_cast<PanelView*>(panel->native_panel())->window();
413  if (!panel_window)
414    return;
415
416  HWND native_panel_window = views::HWNDForWidget(panel_window);
417  HWND native_stack_window =
418      stack_window ? views::HWNDForWidget(stack_window->window_) : NULL;
419
420  // The extended style WS_EX_APPWINDOW is used to force a top-level window onto
421  // the taskbar. In order for multiple stacked panels to appear as one, this
422  // bit needs to be cleared.
423  int value = ::GetWindowLong(native_panel_window, GWL_EXSTYLE);
424  ::SetWindowLong(
425      native_panel_window,
426      GWL_EXSTYLE,
427      native_stack_window ? (value & ~WS_EX_APPWINDOW)
428                          : (value | WS_EX_APPWINDOW));
429
430  // All the windows that share the same owner window will appear as a single
431  // window on the taskbar.
432  ::SetWindowLongPtr(native_panel_window,
433                     GWLP_HWNDPARENT,
434                     reinterpret_cast<LONG>(native_stack_window));
435
436  // Make sure the background stack window always stays behind the panel window.
437  if (native_stack_window) {
438    ::SetWindowPos(native_stack_window, native_panel_window, 0, 0, 0, 0,
439        SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
440  }
441
442#else
443  NOTIMPLEMENTED();
444#endif
445}
446
447views::Widget* PanelStackView::CreateWindowWithBounds(const gfx::Rect& bounds) {
448  views::Widget* window = new views::Widget;
449  views::Widget::InitParams params(views::Widget::InitParams::TYPE_WINDOW);
450  params.delegate = this;
451  params.remove_standard_frame = true;
452  params.bounds = bounds;
453  window->Init(params);
454  window->set_frame_type(views::Widget::FRAME_TYPE_FORCE_CUSTOM);
455  window->set_focus_on_creation(false);
456  window->AddObserver(this);
457  window->ShowInactive();
458
459#if defined(OS_WIN)
460  DCHECK(!panels_.empty());
461  Panel* panel = panels_.front();
462  ui::win::SetAppIdForWindow(
463      ShellIntegration::GetAppModelIdForProfile(UTF8ToWide(panel->app_name()),
464                                                panel->profile()->GetPath()),
465      views::HWNDForWidget(window));
466
467  // Remove the filter for old window in case that we're recreating the window.
468  ui::HWNDSubclass::RemoveFilterFromAllTargets(this);
469
470  // Listen to WM_MOVING message in order to move all panels windows on top of
471  // the background window altogether when the background window is being moved
472  // by the user.
473  ui::HWNDSubclass::AddFilterToTarget(views::HWNDForWidget(window), this);
474#endif
475
476  return window;
477}
478
479void PanelStackView::EnsureWindowCreated() {
480  if (window_)
481    return;
482
483  // Empty size is not allowed so a temporary small size is passed. SetBounds
484  // will be called later to update the bounds.
485  window_ = CreateWindowWithBounds(gfx::Rect(0, 0, 1, 1));
486
487#if defined(OS_WIN)
488  if (base::win::GetVersion() >= base::win::VERSION_WIN7) {
489    HWND native_window = views::HWNDForWidget(window_);
490    thumbnailer_.reset(new TaskbarWindowThumbnailerWin(native_window, this));
491    thumbnailer_->Start();
492  }
493#endif
494}
495
496#if defined(OS_WIN)
497bool PanelStackView::FilterMessage(HWND hwnd,
498                                   UINT message,
499                                   WPARAM w_param,
500                                   LPARAM l_param,
501                                   LRESULT* l_result) {
502  switch (message) {
503    case WM_MOVING:
504      // When the background window is being moved by the user, all panels
505      // should also move.
506      gfx::Rect new_stack_bounds(*(reinterpret_cast<LPRECT>(l_param)));
507      MovePanelsBy(
508          new_stack_bounds.origin() - panels_.front()->GetBounds().origin());
509      break;
510  }
511  return false;
512}
513
514std::vector<HWND> PanelStackView::GetSnapshotWindowHandles() const {
515  std::vector<HWND> native_panel_windows;
516  for (Panels::const_iterator iter = panels_.begin();
517       iter != panels_.end(); ++iter) {
518    Panel* panel = *iter;
519    native_panel_windows.push_back(
520        views::HWNDForWidget(
521            static_cast<PanelView*>(panel->native_panel())->window()));
522  }
523  return native_panel_windows;
524}
525
526void PanelStackView::RefreshLivePreviewThumbnail() {
527  // Don't refresh the thumbnail when the stack window is system minimized
528  // because the snapshot could not be retrieved.
529  if (!thumbnailer_.get() || IsMinimized())
530    return;
531  thumbnailer_->InvalidateSnapshot();
532}
533
534void PanelStackView::DeferUpdateNativeWindowBounds(HDWP defer_window_pos_info,
535                                                   views::Widget* window,
536                                                   const gfx::Rect& bounds) {
537  ::DeferWindowPos(defer_window_pos_info,
538                    views::HWNDForWidget(window),
539                    NULL,
540                    bounds.x(),
541                    bounds.y(),
542                    bounds.width(),
543                    bounds.height(),
544                    SWP_NOACTIVATE | SWP_NOZORDER);
545}
546#endif
547