// 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/macros.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.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/geometry/rect.h" #include "ui/gfx/image/image_skia.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); ~PanelStackWindow() override; // Overridden from views::WidgetDelegate: base::string16 GetWindowTitle() const override; gfx::ImageSkia GetWindowAppIcon() override; gfx::ImageSkia GetWindowIcon() override; views::Widget* GetWidget() override; const views::Widget* GetWidget() const override; // Overridden from views::WidgetObserver: void OnWidgetClosing(views::Widget* widget) override; 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; } } // namespace // 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::OnNativeFocusChanged(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 OnNativeFocusChanged might be called when window_ has not been // 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( shell_integration::GetAppModelIdForProfile( base::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