blob: edbc549735d80736c2c9e973ac02e829e132f9cd [file] [log] [blame]
// 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/views/panels/panel_stack_view.h"
#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/profiles/profile.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 "chrome/browser/ui/views/panels/panel_view.h"
#include "ui/gfx/animation/linear_animation.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/rect.h"
#include "ui/views/widget/widget.h"
#if defined(OS_WIN)
#include "base/win/windows_version.h"
#include "chrome/browser/shell_integration.h"
#include "ui/base/win/shell.h"
#include "ui/views/win/hwnd_util.h"
#endif
namespace {
// These values are experimental and subjective.
const int kDefaultFramerateHz = 50;
const int kSetBoundsAnimationMs = 180;
// The widget window that acts as a background window for the stack of panels.
class PanelStackWindow : public views::WidgetObserver,
public views::WidgetDelegateView {
public:
PanelStackWindow(const gfx::Rect& bounds,
NativePanelStackWindowDelegate* delegate);
virtual ~PanelStackWindow();
// Overridden from views::WidgetDelegate:
virtual base::string16 GetWindowTitle() const OVERRIDE;
virtual gfx::ImageSkia GetWindowAppIcon() OVERRIDE;
virtual gfx::ImageSkia GetWindowIcon() OVERRIDE;
virtual views::Widget* GetWidget() OVERRIDE;
virtual const views::Widget* GetWidget() const OVERRIDE;
// Overridden from views::WidgetObserver:
virtual void OnWidgetClosing(views::Widget* widget) OVERRIDE;
virtual void OnWidgetDestroying(views::Widget* widget) OVERRIDE;
private:
views::Widget* window_; // Weak pointer, own us.
NativePanelStackWindowDelegate* delegate_; // Weak pointer.
DISALLOW_COPY_AND_ASSIGN(PanelStackWindow);
};
PanelStackWindow::PanelStackWindow(const gfx::Rect& bounds,
NativePanelStackWindowDelegate* delegate)
: window_(NULL),
delegate_(delegate) {
window_ = new views::Widget;
views::Widget::InitParams params(views::Widget::InitParams::TYPE_WINDOW);
params.delegate = this;
params.remove_standard_frame = true;
params.bounds = bounds;
window_->Init(params);
window_->set_frame_type(views::Widget::FRAME_TYPE_FORCE_CUSTOM);
window_->set_focus_on_creation(false);
window_->AddObserver(this);
window_->ShowInactive();
}
PanelStackWindow::~PanelStackWindow() {
}
base::string16 PanelStackWindow::GetWindowTitle() const {
return delegate_ ? delegate_->GetTitle() : base::string16();
}
gfx::ImageSkia PanelStackWindow::GetWindowAppIcon() {
if (delegate_) {
gfx::Image app_icon = delegate_->GetIcon();
if (!app_icon.IsEmpty())
return *app_icon.ToImageSkia();
}
return gfx::ImageSkia();
}
gfx::ImageSkia PanelStackWindow::GetWindowIcon() {
return GetWindowAppIcon();
}
views::Widget* PanelStackWindow::GetWidget() {
return window_;
}
const views::Widget* PanelStackWindow::GetWidget() const {
return window_;
}
void PanelStackWindow::OnWidgetClosing(views::Widget* widget) {
delegate_ = NULL;
}
void PanelStackWindow::OnWidgetDestroying(views::Widget* widget) {
window_ = NULL;
}
}
// static
NativePanelStackWindow* NativePanelStackWindow::Create(
NativePanelStackWindowDelegate* delegate) {
#if defined(OS_WIN)
return new PanelStackView(delegate);
#else
NOTIMPLEMENTED();
return NULL;
#endif
}
PanelStackView::PanelStackView(NativePanelStackWindowDelegate* delegate)
: delegate_(delegate),
window_(NULL),
is_drawing_attention_(false),
animate_bounds_updates_(false),
bounds_updates_started_(false) {
DCHECK(delegate);
views::WidgetFocusManager::GetInstance()->AddFocusChangeListener(this);
}
PanelStackView::~PanelStackView() {
#if defined(OS_WIN)
ui::HWNDSubclass::RemoveFilterFromAllTargets(this);
#endif
}
void PanelStackView::Close() {
delegate_ = NULL;
if (bounds_animator_)
bounds_animator_.reset();
if (window_)
window_->Close();
views::WidgetFocusManager::GetInstance()->RemoveFocusChangeListener(this);
}
void PanelStackView::AddPanel(Panel* panel) {
panels_.push_back(panel);
EnsureWindowCreated();
MakeStackWindowOwnPanelWindow(panel, this);
UpdateStackWindowBounds();
window_->UpdateWindowTitle();
window_->UpdateWindowIcon();
}
void PanelStackView::RemovePanel(Panel* panel) {
if (IsAnimatingPanelBounds()) {
// This panel is gone.
bounds_updates_.erase(panel);
// Abort the ongoing animation.
bounds_animator_->Stop();
}
panels_.remove(panel);
MakeStackWindowOwnPanelWindow(panel, NULL);
UpdateStackWindowBounds();
}
void PanelStackView::MergeWith(NativePanelStackWindow* another) {
PanelStackView* another_stack = static_cast<PanelStackView*>(another);
for (Panels::const_iterator iter = another_stack->panels_.begin();
iter != another_stack->panels_.end(); ++iter) {
Panel* panel = *iter;
panels_.push_back(panel);
MakeStackWindowOwnPanelWindow(panel, this);
}
another_stack->panels_.clear();
UpdateStackWindowBounds();
}
bool PanelStackView::IsEmpty() const {
return panels_.empty();
}
bool PanelStackView::HasPanel(Panel* panel) const {
return std::find(panels_.begin(), panels_.end(), panel) != panels_.end();
}
void PanelStackView::MovePanelsBy(const gfx::Vector2d& delta) {
BeginBatchUpdatePanelBounds(false);
for (Panels::const_iterator iter = panels_.begin();
iter != panels_.end(); ++iter) {
Panel* panel = *iter;
AddPanelBoundsForBatchUpdate(panel, panel->GetBounds() + delta);
}
EndBatchUpdatePanelBounds();
}
void PanelStackView::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 PanelStackView::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 valued stored in native panel
// window.
static_cast<PanelView*>(panel->native_panel())->set_cached_bounds_directly(
new_bounds);
}
void PanelStackView::EndBatchUpdatePanelBounds() {
DCHECK(bounds_updates_started_);
if (bounds_updates_.empty() || !animate_bounds_updates_) {
if (!bounds_updates_.empty()) {
UpdatePanelsBounds();
bounds_updates_.clear();
}
bounds_updates_started_ = false;
NotifyBoundsUpdateCompleted();
return;
}
bounds_animator_.reset(new gfx::LinearAnimation(
PanelManager::AdjustTimeInterval(kSetBoundsAnimationMs),
kDefaultFramerateHz,
this));
bounds_animator_->Start();
}
void PanelStackView::NotifyBoundsUpdateCompleted() {
delegate_->PanelBoundsBatchUpdateCompleted();
#if defined(OS_WIN)
// Refresh the thumbnail each time when any bounds updates are done.
RefreshLivePreviewThumbnail();
#endif
}
bool PanelStackView::IsAnimatingPanelBounds() const {
return bounds_updates_started_ && animate_bounds_updates_;
}
void PanelStackView::Minimize() {
#if defined(OS_WIN)
// When the stack window is minimized by the system, its snapshot could not
// be obtained. We need to capture the snapshot before the minimization.
if (thumbnailer_)
thumbnailer_->CaptureSnapshot();
#endif
window_->Minimize();
}
bool PanelStackView::IsMinimized() const {
return window_ ? window_->IsMinimized() : false;
}
void PanelStackView::DrawSystemAttention(bool draw_attention) {
// The underlying call of FlashFrame, FlashWindowEx, seems not to work
// correctly if it is called more than once consecutively.
if (draw_attention == is_drawing_attention_)
return;
is_drawing_attention_ = draw_attention;
#if defined(OS_WIN)
// Refresh the thumbnail when a panel could change something for the
// attention.
RefreshLivePreviewThumbnail();
if (draw_attention) {
// The default implementation of Widget::FlashFrame only flashes 5 times.
// We need more than that.
FLASHWINFO fwi;
fwi.cbSize = sizeof(fwi);
fwi.hwnd = views::HWNDForWidget(window_);
fwi.dwFlags = FLASHW_ALL;
fwi.uCount = panel::kNumberOfTimesToFlashPanelForAttention;
fwi.dwTimeout = 0;
::FlashWindowEx(&fwi);
} else {
// Calling FlashWindowEx with FLASHW_STOP flag does not always work.
// Occasionally the taskbar icon could still remain in the flashed state.
// To work around this problem, we recreate the underlying window.
views::Widget* old_window = window_;
window_ = CreateWindowWithBounds(GetStackWindowBounds());
// New background window should also be minimized if the old one is.
if (old_window->IsMinimized())
window_->Minimize();
// Make sure the new background window stays at the same z-order as the old
// one.
::SetWindowPos(views::HWNDForWidget(window_),
views::HWNDForWidget(old_window),
0, 0, 0, 0,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
for (Panels::const_iterator iter = panels_.begin();
iter != panels_.end(); ++iter) {
MakeStackWindowOwnPanelWindow(*iter, this);
}
// Serve the snapshot to the new backgroud window.
if (thumbnailer_.get())
thumbnailer_->ReplaceWindow(views::HWNDForWidget(window_));
window_->UpdateWindowTitle();
window_->UpdateWindowIcon();
old_window->Close();
}
#else
window_->FlashFrame(draw_attention);
#endif
}
void PanelStackView::OnPanelActivated(Panel* panel) {
// Nothing to do.
}
void PanelStackView::OnNativeFocusChange(gfx::NativeView focused_before,
gfx::NativeView focused_now) {
// When the user selects the stacked panels via ALT-TAB or WIN-TAB, the
// background stack window, instead of the foreground panel window, receives
// WM_SETFOCUS message. To deal with this, we listen to the focus change event
// and activate the most recently active panel.
// Note that OnNativeFocusChange might be called when window_ has not be
// created yet.
#if defined(OS_WIN)
if (!panels_.empty() && window_ && focused_now == window_->GetNativeView()) {
Panel* panel_to_focus =
panels_.front()->stack()->most_recently_active_panel();
if (panel_to_focus)
panel_to_focus->Activate();
}
#endif
}
void PanelStackView::AnimationEnded(const gfx::Animation* animation) {
bounds_updates_started_ = false;
PanelManager* panel_manager = PanelManager::GetInstance();
for (BoundsUpdates::const_iterator iter = bounds_updates_.begin();
iter != bounds_updates_.end(); ++iter) {
panel_manager->OnPanelAnimationEnded(iter->first);
}
bounds_updates_.clear();
NotifyBoundsUpdateCompleted();
}
void PanelStackView::AnimationCanceled(const gfx::Animation* animation) {
// When the animation is aborted due to something like one of panels is gone,
// update panels to their taget bounds immediately.
UpdatePanelsBounds();
AnimationEnded(animation);
}
void PanelStackView::AnimationProgressed(const gfx::Animation* animation) {
UpdatePanelsBounds();
}
void PanelStackView::UpdatePanelsBounds() {
#if defined(OS_WIN)
// Add an extra count for the background stack window.
HDWP defer_update = ::BeginDeferWindowPos(bounds_updates_.size() + 1);
#endif
// Update the bounds for each panel in the update list.
gfx::Rect enclosing_bounds;
for (BoundsUpdates::const_iterator iter = bounds_updates_.begin();
iter != bounds_updates_.end(); ++iter) {
Panel* panel = iter->first;
gfx::Rect target_bounds = panel->GetBounds();
gfx::Rect current_bounds;
if (bounds_animator_ && bounds_animator_->is_animating()) {
current_bounds = bounds_animator_->CurrentValueBetween(
iter->second, target_bounds);
} else {
current_bounds = target_bounds;
}
PanelView* panel_view = static_cast<PanelView*>(panel->native_panel());
#if defined(OS_WIN)
DeferUpdateNativeWindowBounds(defer_update,
panel_view->window(),
current_bounds);
#else
panel_view->SetPanelBoundsInstantly(current_bounds);
#endif
enclosing_bounds = UnionRects(enclosing_bounds, current_bounds);
}
// Compute the stack window bounds that enclose those panels that are not
// in the batch update list.
for (Panels::const_iterator iter = panels_.begin();
iter != panels_.end(); ++iter) {
Panel* panel = *iter;
if (bounds_updates_.find(panel) == bounds_updates_.end())
enclosing_bounds = UnionRects(enclosing_bounds, panel->GetBounds());
}
// Update the bounds of the background stack window.
#if defined(OS_WIN)
DeferUpdateNativeWindowBounds(defer_update, window_, enclosing_bounds);
#else
window_->SetBounds(enclosing_bounds);
#endif
#if defined(OS_WIN)
::EndDeferWindowPos(defer_update);
#endif
}
gfx::Rect PanelStackView::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 PanelStackView::UpdateStackWindowBounds() {
window_->SetBounds(GetStackWindowBounds());
#if defined(OS_WIN)
// Refresh the thumbnail each time whne the stack window is changed, due to
// adding or removing a panel.
RefreshLivePreviewThumbnail();
#endif
}
// static
void PanelStackView::MakeStackWindowOwnPanelWindow(
Panel* panel, PanelStackView* stack_window) {
#if defined(OS_WIN)
// The panel widget window might already be gone when a panel is closed.
views::Widget* panel_window =
static_cast<PanelView*>(panel->native_panel())->window();
if (!panel_window)
return;
HWND native_panel_window = views::HWNDForWidget(panel_window);
HWND native_stack_window =
stack_window ? views::HWNDForWidget(stack_window->window_) : NULL;
// The extended style WS_EX_APPWINDOW is used to force a top-level window onto
// the taskbar. In order for multiple stacked panels to appear as one, this
// bit needs to be cleared.
int value = ::GetWindowLong(native_panel_window, GWL_EXSTYLE);
::SetWindowLong(
native_panel_window,
GWL_EXSTYLE,
native_stack_window ? (value & ~WS_EX_APPWINDOW)
: (value | WS_EX_APPWINDOW));
// All the windows that share the same owner window will appear as a single
// window on the taskbar.
::SetWindowLongPtr(native_panel_window,
GWLP_HWNDPARENT,
reinterpret_cast<LONG_PTR>(native_stack_window));
// Make sure the background stack window always stays behind the panel window.
if (native_stack_window) {
::SetWindowPos(native_stack_window, native_panel_window, 0, 0, 0, 0,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
}
#else
NOTIMPLEMENTED();
#endif
}
views::Widget* PanelStackView::CreateWindowWithBounds(const gfx::Rect& bounds) {
PanelStackWindow* stack_window = new PanelStackWindow(bounds, delegate_);
views::Widget* window = stack_window->GetWidget();
#if defined(OS_WIN)
DCHECK(!panels_.empty());
Panel* panel = panels_.front();
ui::win::SetAppIdForWindow(
ShellIntegration::GetAppModelIdForProfile(UTF8ToWide(panel->app_name()),
panel->profile()->GetPath()),
views::HWNDForWidget(window));
// Remove the filter for old window in case that we're recreating the window.
ui::HWNDSubclass::RemoveFilterFromAllTargets(this);
// Listen to WM_MOVING message in order to move all panels windows on top of
// the background window altogether when the background window is being moved
// by the user.
ui::HWNDSubclass::AddFilterToTarget(views::HWNDForWidget(window), this);
#endif
return window;
}
void PanelStackView::EnsureWindowCreated() {
if (window_)
return;
// Empty size is not allowed so a temporary small size is passed. SetBounds
// will be called later to update the bounds.
window_ = CreateWindowWithBounds(gfx::Rect(0, 0, 1, 1));
#if defined(OS_WIN)
if (base::win::GetVersion() >= base::win::VERSION_WIN7) {
HWND native_window = views::HWNDForWidget(window_);
thumbnailer_.reset(new TaskbarWindowThumbnailerWin(native_window, this));
thumbnailer_->Start();
}
#endif
}
#if defined(OS_WIN)
bool PanelStackView::FilterMessage(HWND hwnd,
UINT message,
WPARAM w_param,
LPARAM l_param,
LRESULT* l_result) {
switch (message) {
case WM_MOVING:
// When the background window is being moved by the user, all panels
// should also move.
gfx::Rect new_stack_bounds(*(reinterpret_cast<LPRECT>(l_param)));
MovePanelsBy(
new_stack_bounds.origin() - panels_.front()->GetBounds().origin());
break;
}
return false;
}
std::vector<HWND> PanelStackView::GetSnapshotWindowHandles() const {
std::vector<HWND> native_panel_windows;
for (Panels::const_iterator iter = panels_.begin();
iter != panels_.end(); ++iter) {
Panel* panel = *iter;
native_panel_windows.push_back(
views::HWNDForWidget(
static_cast<PanelView*>(panel->native_panel())->window()));
}
return native_panel_windows;
}
void PanelStackView::RefreshLivePreviewThumbnail() {
// Don't refresh the thumbnail when the stack window is system minimized
// because the snapshot could not be retrieved.
if (!thumbnailer_.get() || IsMinimized())
return;
thumbnailer_->InvalidateSnapshot();
}
void PanelStackView::DeferUpdateNativeWindowBounds(HDWP defer_window_pos_info,
views::Widget* window,
const gfx::Rect& bounds) {
::DeferWindowPos(defer_window_pos_info,
views::HWNDForWidget(window),
NULL,
bounds.x(),
bounds.y(),
bounds.width(),
bounds.height(),
SWP_NOACTIVATE | SWP_NOZORDER);
}
#endif