// Copyright (c) 2012 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 "ash/wm/workspace/multi_window_resize_controller.h" #include "ash/screen_util.h" #include "ash/shell.h" #include "ash/shell_window_ids.h" #include "ash/wm/window_animations.h" #include "ash/wm/workspace/workspace_event_handler.h" #include "ash/wm/workspace/workspace_window_resizer.h" #include "grit/ash_resources.h" #include "ui/aura/client/screen_position_client.h" #include "ui/aura/window.h" #include "ui/aura/window_delegate.h" #include "ui/aura/window_event_dispatcher.h" #include "ui/base/hit_test.h" #include "ui/base/resource/resource_bundle.h" #include "ui/events/event_targeter.h" #include "ui/events/event_utils.h" #include "ui/gfx/canvas.h" #include "ui/gfx/image/image.h" #include "ui/gfx/screen.h" #include "ui/views/view.h" #include "ui/views/widget/widget.h" #include "ui/views/widget/widget_delegate.h" #include "ui/wm/core/compound_event_filter.h" #include "ui/wm/core/coordinate_conversion.h" using aura::Window; namespace ash { namespace { // Delay before showing. const int kShowDelayMS = 400; // Delay before hiding. const int kHideDelayMS = 500; // Padding from the bottom/right edge the resize widget is shown at. const int kResizeWidgetPadding = 15; bool ContainsX(Window* window, int x) { return x >= 0 && x <= window->bounds().width(); } bool ContainsScreenX(Window* window, int x_in_screen) { gfx::Point window_loc(x_in_screen, 0); ::wm::ConvertPointFromScreen(window, &window_loc); return ContainsX(window, window_loc.x()); } bool ContainsY(Window* window, int y) { return y >= 0 && y <= window->bounds().height(); } bool ContainsScreenY(Window* window, int y_in_screen) { gfx::Point window_loc(0, y_in_screen); ::wm::ConvertPointFromScreen(window, &window_loc); return ContainsY(window, window_loc.y()); } bool Intersects(int x1, int max_1, int x2, int max_2) { return x2 <= max_1 && max_2 > x1; } } // namespace // View contained in the widget. Passes along mouse events to the // MultiWindowResizeController so that it can start/stop the resize loop. class MultiWindowResizeController::ResizeView : public views::View { public: explicit ResizeView(MultiWindowResizeController* controller, Direction direction) : controller_(controller), direction_(direction), image_(NULL) { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); int image_id = direction == TOP_BOTTOM ? IDR_AURA_MULTI_WINDOW_RESIZE_H : IDR_AURA_MULTI_WINDOW_RESIZE_V; image_ = rb.GetImageNamed(image_id).ToImageSkia(); } // views::View overrides: gfx::Size GetPreferredSize() const override { return gfx::Size(image_->width(), image_->height()); } void OnPaint(gfx::Canvas* canvas) override { canvas->DrawImageInt(*image_, 0, 0); } bool OnMousePressed(const ui::MouseEvent& event) override { gfx::Point location(event.location()); views::View::ConvertPointToScreen(this, &location); controller_->StartResize(location); return true; } bool OnMouseDragged(const ui::MouseEvent& event) override { gfx::Point location(event.location()); views::View::ConvertPointToScreen(this, &location); controller_->Resize(location, event.flags()); return true; } void OnMouseReleased(const ui::MouseEvent& event) override { controller_->CompleteResize(); } void OnMouseCaptureLost() override { controller_->CancelResize(); } gfx::NativeCursor GetCursor(const ui::MouseEvent& event) override { int component = (direction_ == LEFT_RIGHT) ? HTRIGHT : HTBOTTOM; return ::wm::CompoundEventFilter::CursorForWindowComponent( component); } private: MultiWindowResizeController* controller_; const Direction direction_; const gfx::ImageSkia* image_; DISALLOW_COPY_AND_ASSIGN(ResizeView); }; // MouseWatcherHost implementation for MultiWindowResizeController. Forwards // Contains() to MultiWindowResizeController. class MultiWindowResizeController::ResizeMouseWatcherHost : public views::MouseWatcherHost { public: ResizeMouseWatcherHost(MultiWindowResizeController* host) : host_(host) {} // MouseWatcherHost overrides: bool Contains(const gfx::Point& point_in_screen, MouseEventType type) override { return (type == MOUSE_PRESS) ? host_->IsOverResizeWidget(point_in_screen) : host_->IsOverWindows(point_in_screen); } private: MultiWindowResizeController* host_; DISALLOW_COPY_AND_ASSIGN(ResizeMouseWatcherHost); }; MultiWindowResizeController::ResizeWindows::ResizeWindows() : window1(NULL), window2(NULL), direction(TOP_BOTTOM){ } MultiWindowResizeController::ResizeWindows::~ResizeWindows() { } bool MultiWindowResizeController::ResizeWindows::Equals( const ResizeWindows& other) const { return window1 == other.window1 && window2 == other.window2 && direction == other.direction; } MultiWindowResizeController::MultiWindowResizeController() { } MultiWindowResizeController::~MultiWindowResizeController() { window_resizer_.reset(); Hide(); } void MultiWindowResizeController::Show(Window* window, int component, const gfx::Point& point_in_window) { // When the resize widget is showing we ignore Show() requests. Instead we // only care about mouse movements from MouseWatcher. This is necessary as // WorkspaceEventHandler only sees mouse movements over the windows, not all // windows or over the desktop. if (resize_widget_) return; ResizeWindows windows(DetermineWindows(window, component, point_in_window)); if (IsShowing() && windows_.Equals(windows)) return; Hide(); if (!windows.is_valid()) { windows_ = ResizeWindows(); return; } windows_ = windows; windows_.window1->AddObserver(this); windows_.window2->AddObserver(this); show_location_in_parent_ = point_in_window; Window::ConvertPointToTarget( window, window->parent(), &show_location_in_parent_); show_timer_.Start( FROM_HERE, base::TimeDelta::FromMilliseconds(kShowDelayMS), this, &MultiWindowResizeController::ShowIfValidMouseLocation); } void MultiWindowResizeController::Hide() { if (window_resizer_) return; // Ignore hides while actively resizing. if (windows_.window1) { windows_.window1->RemoveObserver(this); windows_.window1 = NULL; } if (windows_.window2) { windows_.window2->RemoveObserver(this); windows_.window2 = NULL; } show_timer_.Stop(); if (!resize_widget_) return; for (size_t i = 0; i < windows_.other_windows.size(); ++i) windows_.other_windows[i]->RemoveObserver(this); mouse_watcher_.reset(); resize_widget_.reset(); windows_ = ResizeWindows(); } void MultiWindowResizeController::MouseMovedOutOfHost() { Hide(); } void MultiWindowResizeController::OnWindowDestroying( aura::Window* window) { // Have to explicitly reset the WindowResizer, otherwise Hide() does nothing. window_resizer_.reset(); Hide(); } MultiWindowResizeController::ResizeWindows MultiWindowResizeController::DetermineWindowsFromScreenPoint( aura::Window* window) const { gfx::Point mouse_location( gfx::Screen::GetScreenFor(window)->GetCursorScreenPoint()); ::wm::ConvertPointFromScreen(window, &mouse_location); const int component = window->delegate()->GetNonClientComponent(mouse_location); return DetermineWindows(window, component, mouse_location); } void MultiWindowResizeController::CreateMouseWatcher() { mouse_watcher_.reset(new views::MouseWatcher( new ResizeMouseWatcherHost(this), this)); mouse_watcher_->set_notify_on_exit_time( base::TimeDelta::FromMilliseconds(kHideDelayMS)); mouse_watcher_->Start(); } MultiWindowResizeController::ResizeWindows MultiWindowResizeController::DetermineWindows( Window* window, int window_component, const gfx::Point& point) const { ResizeWindows result; gfx::Point point_in_parent(point); Window::ConvertPointToTarget(window, window->parent(), &point_in_parent); switch (window_component) { case HTRIGHT: result.direction = LEFT_RIGHT; result.window1 = window; result.window2 = FindWindowByEdge( window, HTLEFT, window->bounds().right(), point_in_parent.y()); break; case HTLEFT: result.direction = LEFT_RIGHT; result.window1 = FindWindowByEdge( window, HTRIGHT, window->bounds().x(), point_in_parent.y()); result.window2 = window; break; case HTTOP: result.direction = TOP_BOTTOM; result.window1 = FindWindowByEdge( window, HTBOTTOM, point_in_parent.x(), window->bounds().y()); result.window2 = window; break; case HTBOTTOM: result.direction = TOP_BOTTOM; result.window1 = window; result.window2 = FindWindowByEdge( window, HTTOP, point_in_parent.x(), window->bounds().bottom()); break; default: break; } return result; } Window* MultiWindowResizeController::FindWindowByEdge( Window* window_to_ignore, int edge_want, int x_in_parent, int y_in_parent) const { Window* parent = window_to_ignore->parent(); const Window::Windows& windows(parent->children()); for (Window::Windows::const_reverse_iterator i = windows.rbegin(); i != windows.rend(); ++i) { Window* window = *i; if (window == window_to_ignore || !window->IsVisible()) continue; // Ignore windows without a delegate. A delegate is necessary to query the // non-client component. if (!window->delegate()) continue; gfx::Point p(x_in_parent, y_in_parent); aura::Window::ConvertPointToTarget(parent, window, &p); switch (edge_want) { case HTLEFT: if (ContainsY(window, p.y()) && p.x() == 0) return window; break; case HTRIGHT: if (ContainsY(window, p.y()) && p.x() == window->bounds().width()) return window; break; case HTTOP: if (ContainsX(window, p.x()) && p.y() == 0) return window; break; case HTBOTTOM: if (ContainsX(window, p.x()) && p.y() == window->bounds().height()) return window; break; default: NOTREACHED(); } // Window doesn't contain the edge, but if window contains |point| // it's obscuring any other window that could be at the location. if (window->bounds().Contains(x_in_parent, y_in_parent)) return NULL; } return NULL; } aura::Window* MultiWindowResizeController::FindWindowTouching( aura::Window* window, Direction direction) const { int right = window->bounds().right(); int bottom = window->bounds().bottom(); Window* parent = window->parent(); const Window::Windows& windows(parent->children()); for (Window::Windows::const_reverse_iterator i = windows.rbegin(); i != windows.rend(); ++i) { Window* other = *i; if (other == window || !other->IsVisible()) continue; switch (direction) { case TOP_BOTTOM: if (other->bounds().y() == bottom && Intersects(other->bounds().x(), other->bounds().right(), window->bounds().x(), window->bounds().right())) { return other; } break; case LEFT_RIGHT: if (other->bounds().x() == right && Intersects(other->bounds().y(), other->bounds().bottom(), window->bounds().y(), window->bounds().bottom())) { return other; } break; default: NOTREACHED(); } } return NULL; } void MultiWindowResizeController::FindWindowsTouching( aura::Window* start, Direction direction, std::vector* others) const { while (start) { start = FindWindowTouching(start, direction); if (start) others->push_back(start); } } void MultiWindowResizeController::ShowIfValidMouseLocation() { if (DetermineWindowsFromScreenPoint(windows_.window1).Equals(windows_) || DetermineWindowsFromScreenPoint(windows_.window2).Equals(windows_)) { ShowNow(); } else { Hide(); } } void MultiWindowResizeController::ShowNow() { DCHECK(!resize_widget_.get()); DCHECK(windows_.is_valid()); show_timer_.Stop(); resize_widget_.reset(new views::Widget); views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; params.parent = Shell::GetContainer(Shell::GetTargetRootWindow(), kShellWindowId_AlwaysOnTopContainer); ResizeView* view = new ResizeView(this, windows_.direction); resize_widget_->set_focus_on_creation(false); resize_widget_->Init(params); ::wm::SetWindowVisibilityAnimationType( resize_widget_->GetNativeWindow(), ::wm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE); resize_widget_->GetNativeWindow()->SetName("MultiWindowResizeController"); resize_widget_->SetContentsView(view); show_bounds_in_screen_ = ScreenUtil::ConvertRectToScreen( windows_.window1->parent(), CalculateResizeWidgetBounds(show_location_in_parent_)); resize_widget_->SetBounds(show_bounds_in_screen_); resize_widget_->Show(); CreateMouseWatcher(); } bool MultiWindowResizeController::IsShowing() const { return resize_widget_.get() || show_timer_.IsRunning(); } void MultiWindowResizeController::StartResize( const gfx::Point& location_in_screen) { DCHECK(!window_resizer_.get()); DCHECK(windows_.is_valid()); gfx::Point location_in_parent(location_in_screen); aura::client::GetScreenPositionClient(windows_.window2->GetRootWindow())-> ConvertPointFromScreen(windows_.window2->parent(), &location_in_parent); std::vector windows; windows.push_back(windows_.window2); DCHECK(windows_.other_windows.empty()); FindWindowsTouching(windows_.window2, windows_.direction, &windows_.other_windows); for (size_t i = 0; i < windows_.other_windows.size(); ++i) { windows_.other_windows[i]->AddObserver(this); windows.push_back(windows_.other_windows[i]); } int component = windows_.direction == LEFT_RIGHT ? HTRIGHT : HTBOTTOM; wm::WindowState* window_state = wm::GetWindowState(windows_.window1); window_state->CreateDragDetails(windows_.window1, location_in_parent, component, aura::client::WINDOW_MOVE_SOURCE_MOUSE); window_resizer_.reset(WorkspaceWindowResizer::Create(window_state, windows)); // Do not hide the resize widget while a drag is active. mouse_watcher_.reset(); } void MultiWindowResizeController::Resize(const gfx::Point& location_in_screen, int event_flags) { gfx::Point location_in_parent(location_in_screen); aura::client::GetScreenPositionClient(windows_.window1->GetRootWindow())-> ConvertPointFromScreen(windows_.window1->parent(), &location_in_parent); window_resizer_->Drag(location_in_parent, event_flags); gfx::Rect bounds = ScreenUtil::ConvertRectToScreen( windows_.window1->parent(), CalculateResizeWidgetBounds(location_in_parent)); if (windows_.direction == LEFT_RIGHT) bounds.set_y(show_bounds_in_screen_.y()); else bounds.set_x(show_bounds_in_screen_.x()); resize_widget_->SetBounds(bounds); } void MultiWindowResizeController::CompleteResize() { window_resizer_->CompleteDrag(); wm::GetWindowState(window_resizer_->GetTarget())->DeleteDragDetails(); window_resizer_.reset(); // Mouse may still be over resizer, if not hide. gfx::Point screen_loc = Shell::GetScreen()->GetCursorScreenPoint(); if (!resize_widget_->GetWindowBoundsInScreen().Contains(screen_loc)) { Hide(); } else { // If the mouse is over the resizer we need to remove observers on any of // the |other_windows|. If we start another resize we'll recalculate the // |other_windows| and invoke AddObserver() as necessary. for (size_t i = 0; i < windows_.other_windows.size(); ++i) windows_.other_windows[i]->RemoveObserver(this); windows_.other_windows.clear(); CreateMouseWatcher(); } } void MultiWindowResizeController::CancelResize() { if (!window_resizer_) return; // Happens if window was destroyed and we nuked the WindowResizer. window_resizer_->RevertDrag(); wm::GetWindowState(window_resizer_->GetTarget())->DeleteDragDetails(); window_resizer_.reset(); Hide(); } gfx::Rect MultiWindowResizeController::CalculateResizeWidgetBounds( const gfx::Point& location_in_parent) const { gfx::Size pref = resize_widget_->GetContentsView()->GetPreferredSize(); int x = 0, y = 0; if (windows_.direction == LEFT_RIGHT) { x = windows_.window1->bounds().right() - pref.width() / 2; y = location_in_parent.y() + kResizeWidgetPadding; if (y + pref.height() / 2 > windows_.window1->bounds().bottom() && y + pref.height() / 2 > windows_.window2->bounds().bottom()) { y = location_in_parent.y() - kResizeWidgetPadding - pref.height(); } } else { x = location_in_parent.x() + kResizeWidgetPadding; if (x + pref.height() / 2 > windows_.window1->bounds().right() && x + pref.height() / 2 > windows_.window2->bounds().right()) { x = location_in_parent.x() - kResizeWidgetPadding - pref.width(); } y = windows_.window1->bounds().bottom() - pref.height() / 2; } return gfx::Rect(x, y, pref.width(), pref.height()); } bool MultiWindowResizeController::IsOverResizeWidget( const gfx::Point& location_in_screen) const { return resize_widget_->GetWindowBoundsInScreen().Contains( location_in_screen); } bool MultiWindowResizeController::IsOverWindows( const gfx::Point& location_in_screen) const { if (IsOverResizeWidget(location_in_screen)) return true; if (windows_.direction == TOP_BOTTOM) { if (!ContainsScreenX(windows_.window1, location_in_screen.x()) || !ContainsScreenX(windows_.window2, location_in_screen.x())) { return false; } } else { if (!ContainsScreenY(windows_.window1, location_in_screen.y()) || !ContainsScreenY(windows_.window2, location_in_screen.y())) { return false; } } // Check whether |location_in_screen| is in the event target's resize region. // This is tricky because a window's resize region can extend outside a // window's bounds. gfx::Point location_in_root(location_in_screen); aura::Window* root = windows_.window1->GetRootWindow(); ::wm::ConvertPointFromScreen(root, &location_in_root); ui::MouseEvent test_event(ui::ET_MOUSE_MOVED, location_in_root, location_in_root, ui::EventTimeForNow(), ui::EF_NONE, ui::EF_NONE); ui::EventTarget* event_handler = static_cast(root) ->GetEventTargeter() ->FindTargetForEvent(root, &test_event); if (event_handler == windows_.window1) { return IsOverComponent( windows_.window1, location_in_screen, windows_.direction == TOP_BOTTOM ? HTBOTTOM : HTRIGHT); } else if (event_handler == windows_.window2) { return IsOverComponent( windows_.window2, location_in_screen, windows_.direction == TOP_BOTTOM ? HTTOP : HTLEFT); } return false; } bool MultiWindowResizeController::IsOverComponent( aura::Window* window, const gfx::Point& location_in_screen, int component) const { gfx::Point window_loc(location_in_screen); ::wm::ConvertPointFromScreen(window, &window_loc); return window->delegate()->GetNonClientComponent(window_loc) == component; } } // namespace ash