1// Copyright (c) 2012 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 "ash/wm/workspace/multi_window_resize_controller.h"
6
7#include "ash/screen_util.h"
8#include "ash/shell.h"
9#include "ash/shell_window_ids.h"
10#include "ash/wm/window_animations.h"
11#include "ash/wm/workspace/workspace_event_handler.h"
12#include "ash/wm/workspace/workspace_window_resizer.h"
13#include "grit/ash_resources.h"
14#include "ui/aura/client/screen_position_client.h"
15#include "ui/aura/window.h"
16#include "ui/aura/window_delegate.h"
17#include "ui/aura/window_event_dispatcher.h"
18#include "ui/base/hit_test.h"
19#include "ui/base/resource/resource_bundle.h"
20#include "ui/gfx/canvas.h"
21#include "ui/gfx/image/image.h"
22#include "ui/gfx/screen.h"
23#include "ui/views/view.h"
24#include "ui/views/widget/widget.h"
25#include "ui/views/widget/widget_delegate.h"
26#include "ui/wm/core/compound_event_filter.h"
27#include "ui/wm/core/coordinate_conversion.h"
28
29using aura::Window;
30
31namespace ash {
32namespace {
33
34// Delay before showing.
35const int kShowDelayMS = 400;
36
37// Delay before hiding.
38const int kHideDelayMS = 500;
39
40// Padding from the bottom/right edge the resize widget is shown at.
41const int kResizeWidgetPadding = 15;
42
43bool ContainsX(Window* window, int x) {
44  return window->bounds().x() <= x && window->bounds().right() >= x;
45}
46
47bool ContainsY(Window* window, int y) {
48  return window->bounds().y() <= y && window->bounds().bottom() >= y;
49}
50
51bool Intersects(int x1, int max_1, int x2, int max_2) {
52  return x2 <= max_1 && max_2 > x1;
53}
54
55}  // namespace
56
57// View contained in the widget. Passes along mouse events to the
58// MultiWindowResizeController so that it can start/stop the resize loop.
59class MultiWindowResizeController::ResizeView : public views::View {
60 public:
61  explicit ResizeView(MultiWindowResizeController* controller,
62                      Direction direction)
63      : controller_(controller),
64        direction_(direction),
65        image_(NULL) {
66    ResourceBundle& rb = ResourceBundle::GetSharedInstance();
67    int image_id =
68        direction == TOP_BOTTOM ? IDR_AURA_MULTI_WINDOW_RESIZE_H :
69                                  IDR_AURA_MULTI_WINDOW_RESIZE_V;
70    image_ = rb.GetImageNamed(image_id).ToImageSkia();
71  }
72
73  // views::View overrides:
74  virtual gfx::Size GetPreferredSize() const OVERRIDE {
75    return gfx::Size(image_->width(), image_->height());
76  }
77  virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE {
78    canvas->DrawImageInt(*image_, 0, 0);
79  }
80  virtual bool OnMousePressed(const ui::MouseEvent& event) OVERRIDE {
81    gfx::Point location(event.location());
82    views::View::ConvertPointToScreen(this, &location);
83    controller_->StartResize(location);
84    return true;
85  }
86  virtual bool OnMouseDragged(const ui::MouseEvent& event) OVERRIDE {
87    gfx::Point location(event.location());
88    views::View::ConvertPointToScreen(this, &location);
89    controller_->Resize(location, event.flags());
90    return true;
91  }
92  virtual void OnMouseReleased(const ui::MouseEvent& event) OVERRIDE {
93    controller_->CompleteResize();
94  }
95  virtual void OnMouseCaptureLost() OVERRIDE {
96    controller_->CancelResize();
97  }
98  virtual gfx::NativeCursor GetCursor(
99      const ui::MouseEvent& event) OVERRIDE {
100    int component = (direction_ == LEFT_RIGHT) ? HTRIGHT : HTBOTTOM;
101    return ::wm::CompoundEventFilter::CursorForWindowComponent(
102        component);
103  }
104
105 private:
106  MultiWindowResizeController* controller_;
107  const Direction direction_;
108  const gfx::ImageSkia* image_;
109
110  DISALLOW_COPY_AND_ASSIGN(ResizeView);
111};
112
113// MouseWatcherHost implementation for MultiWindowResizeController. Forwards
114// Contains() to MultiWindowResizeController.
115class MultiWindowResizeController::ResizeMouseWatcherHost :
116   public views::MouseWatcherHost {
117 public:
118  ResizeMouseWatcherHost(MultiWindowResizeController* host) : host_(host) {}
119
120  // MouseWatcherHost overrides:
121  virtual bool Contains(const gfx::Point& point_in_screen,
122                        MouseEventType type) OVERRIDE {
123    return host_->IsOverWindows(point_in_screen);
124  }
125
126 private:
127  MultiWindowResizeController* host_;
128
129  DISALLOW_COPY_AND_ASSIGN(ResizeMouseWatcherHost);
130};
131
132MultiWindowResizeController::ResizeWindows::ResizeWindows()
133    : window1(NULL),
134      window2(NULL),
135      direction(TOP_BOTTOM){
136}
137
138MultiWindowResizeController::ResizeWindows::~ResizeWindows() {
139}
140
141bool MultiWindowResizeController::ResizeWindows::Equals(
142    const ResizeWindows& other) const {
143  return window1 == other.window1 &&
144      window2 == other.window2 &&
145      direction == other.direction;
146}
147
148MultiWindowResizeController::MultiWindowResizeController() {
149}
150
151MultiWindowResizeController::~MultiWindowResizeController() {
152  window_resizer_.reset();
153  Hide();
154}
155
156void MultiWindowResizeController::Show(Window* window,
157                                       int component,
158                                       const gfx::Point& point_in_window) {
159  // When the resize widget is showing we ignore Show() requests. Instead we
160  // only care about mouse movements from MouseWatcher. This is necessary as
161  // WorkspaceEventHandler only sees mouse movements over the windows, not all
162  // windows or over the desktop.
163  if (resize_widget_)
164    return;
165
166  ResizeWindows windows(DetermineWindows(window, component, point_in_window));
167  if (IsShowing()) {
168    if (windows_.Equals(windows))
169      return;  // Over the same windows.
170    DelayedHide();
171  }
172
173  if (!windows.is_valid())
174    return;
175  Hide();
176  windows_ = windows;
177  windows_.window1->AddObserver(this);
178  windows_.window2->AddObserver(this);
179  show_location_in_parent_ = point_in_window;
180  Window::ConvertPointToTarget(
181      window, window->parent(), &show_location_in_parent_);
182  if (show_timer_.IsRunning())
183    return;
184  show_timer_.Start(
185      FROM_HERE, base::TimeDelta::FromMilliseconds(kShowDelayMS),
186      this, &MultiWindowResizeController::ShowIfValidMouseLocation);
187}
188
189void MultiWindowResizeController::Hide() {
190  hide_timer_.Stop();
191  if (window_resizer_)
192    return;  // Ignore hides while actively resizing.
193
194  if (windows_.window1) {
195    windows_.window1->RemoveObserver(this);
196    windows_.window1 = NULL;
197  }
198  if (windows_.window2) {
199    windows_.window2->RemoveObserver(this);
200    windows_.window2 = NULL;
201  }
202
203  show_timer_.Stop();
204
205  if (!resize_widget_)
206    return;
207
208  for (size_t i = 0; i < windows_.other_windows.size(); ++i)
209    windows_.other_windows[i]->RemoveObserver(this);
210  mouse_watcher_.reset();
211  resize_widget_.reset();
212  windows_ = ResizeWindows();
213}
214
215void MultiWindowResizeController::MouseMovedOutOfHost() {
216  Hide();
217}
218
219void MultiWindowResizeController::OnWindowDestroying(
220    aura::Window* window) {
221  // Have to explicitly reset the WindowResizer, otherwise Hide() does nothing.
222  window_resizer_.reset();
223  Hide();
224}
225
226MultiWindowResizeController::ResizeWindows
227MultiWindowResizeController::DetermineWindowsFromScreenPoint(
228    aura::Window* window) const {
229  gfx::Point mouse_location(
230      gfx::Screen::GetScreenFor(window)->GetCursorScreenPoint());
231  ::wm::ConvertPointFromScreen(window, &mouse_location);
232  const int component =
233      window->delegate()->GetNonClientComponent(mouse_location);
234  return DetermineWindows(window, component, mouse_location);
235}
236
237MultiWindowResizeController::ResizeWindows
238MultiWindowResizeController::DetermineWindows(
239    Window* window,
240    int window_component,
241    const gfx::Point& point) const {
242  ResizeWindows result;
243  gfx::Point point_in_parent(point);
244  Window::ConvertPointToTarget(window, window->parent(), &point_in_parent);
245  switch (window_component) {
246    case HTRIGHT:
247      result.direction = LEFT_RIGHT;
248      result.window1 = window;
249      result.window2 = FindWindowByEdge(
250          window, HTLEFT, window->bounds().right(), point_in_parent.y());
251      break;
252    case HTLEFT:
253      result.direction = LEFT_RIGHT;
254      result.window1 = FindWindowByEdge(
255          window, HTRIGHT, window->bounds().x(), point_in_parent.y());
256      result.window2 = window;
257      break;
258    case HTTOP:
259      result.direction = TOP_BOTTOM;
260      result.window1 = FindWindowByEdge(
261          window, HTBOTTOM, point_in_parent.x(), window->bounds().y());
262      result.window2 = window;
263      break;
264    case HTBOTTOM:
265      result.direction = TOP_BOTTOM;
266      result.window1 = window;
267      result.window2 = FindWindowByEdge(
268          window, HTTOP, point_in_parent.x(), window->bounds().bottom());
269      break;
270    default:
271      break;
272  }
273  return result;
274}
275
276Window* MultiWindowResizeController::FindWindowByEdge(
277    Window* window_to_ignore,
278    int edge_want,
279    int x,
280    int y) const {
281  Window* parent = window_to_ignore->parent();
282  const Window::Windows& windows(parent->children());
283  for (Window::Windows::const_reverse_iterator i = windows.rbegin();
284       i != windows.rend(); ++i) {
285    Window* window = *i;
286    if (window == window_to_ignore || !window->IsVisible())
287      continue;
288    switch (edge_want) {
289      case HTLEFT:
290        if (ContainsY(window, y) && window->bounds().x() == x)
291          return window;
292        break;
293      case HTRIGHT:
294        if (ContainsY(window, y) && window->bounds().right() == x)
295          return window;
296        break;
297      case HTTOP:
298        if (ContainsX(window, x) && window->bounds().y() == y)
299          return window;
300        break;
301      case HTBOTTOM:
302        if (ContainsX(window, x) && window->bounds().bottom() == y)
303          return window;
304        break;
305      default:
306        NOTREACHED();
307    }
308    // Window doesn't contain the edge, but if window contains |point|
309    // it's obscuring any other window that could be at the location.
310    if (window->bounds().Contains(x, y))
311      return NULL;
312  }
313  return NULL;
314}
315
316aura::Window* MultiWindowResizeController::FindWindowTouching(
317    aura::Window* window,
318    Direction direction) const {
319  int right = window->bounds().right();
320  int bottom = window->bounds().bottom();
321  Window* parent = window->parent();
322  const Window::Windows& windows(parent->children());
323  for (Window::Windows::const_reverse_iterator i = windows.rbegin();
324       i != windows.rend(); ++i) {
325    Window* other = *i;
326    if (other == window || !other->IsVisible())
327      continue;
328    switch (direction) {
329      case TOP_BOTTOM:
330        if (other->bounds().y() == bottom &&
331            Intersects(other->bounds().x(), other->bounds().right(),
332                       window->bounds().x(), window->bounds().right())) {
333          return other;
334        }
335        break;
336      case LEFT_RIGHT:
337        if (other->bounds().x() == right &&
338            Intersects(other->bounds().y(), other->bounds().bottom(),
339                       window->bounds().y(), window->bounds().bottom())) {
340          return other;
341        }
342        break;
343      default:
344        NOTREACHED();
345    }
346  }
347  return NULL;
348}
349
350void MultiWindowResizeController::FindWindowsTouching(
351    aura::Window* start,
352    Direction direction,
353    std::vector<aura::Window*>* others) const {
354  while (start) {
355    start = FindWindowTouching(start, direction);
356    if (start)
357      others->push_back(start);
358  }
359}
360
361void MultiWindowResizeController::DelayedHide() {
362  if (hide_timer_.IsRunning())
363    return;
364
365  hide_timer_.Start(FROM_HERE,
366                    base::TimeDelta::FromMilliseconds(kHideDelayMS),
367                    this, &MultiWindowResizeController::Hide);
368}
369
370void MultiWindowResizeController::ShowIfValidMouseLocation() {
371  if (DetermineWindowsFromScreenPoint(windows_.window1).Equals(windows_) ||
372      DetermineWindowsFromScreenPoint(windows_.window2).Equals(windows_)) {
373    ShowNow();
374  } else {
375    Hide();
376  }
377}
378
379void MultiWindowResizeController::ShowNow() {
380  DCHECK(!resize_widget_.get());
381  DCHECK(windows_.is_valid());
382  show_timer_.Stop();
383  resize_widget_.reset(new views::Widget);
384  views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
385  params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
386  params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
387  params.parent = Shell::GetContainer(Shell::GetTargetRootWindow(),
388                                      kShellWindowId_AlwaysOnTopContainer);
389  ResizeView* view = new ResizeView(this, windows_.direction);
390  resize_widget_->set_focus_on_creation(false);
391  resize_widget_->Init(params);
392  ::wm::SetWindowVisibilityAnimationType(
393      resize_widget_->GetNativeWindow(),
394      ::wm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE);
395  resize_widget_->GetNativeWindow()->SetName("MultiWindowResizeController");
396  resize_widget_->SetContentsView(view);
397  show_bounds_in_screen_ = ScreenUtil::ConvertRectToScreen(
398      windows_.window1->parent(),
399      CalculateResizeWidgetBounds(show_location_in_parent_));
400  resize_widget_->SetBounds(show_bounds_in_screen_);
401  resize_widget_->Show();
402  mouse_watcher_.reset(new views::MouseWatcher(
403                           new ResizeMouseWatcherHost(this),
404                           this));
405  mouse_watcher_->set_notify_on_exit_time(
406      base::TimeDelta::FromMilliseconds(kHideDelayMS));
407  mouse_watcher_->Start();
408}
409
410bool MultiWindowResizeController::IsShowing() const {
411  return resize_widget_.get() || show_timer_.IsRunning();
412}
413
414void MultiWindowResizeController::StartResize(
415    const gfx::Point& location_in_screen) {
416  DCHECK(!window_resizer_.get());
417  DCHECK(windows_.is_valid());
418  hide_timer_.Stop();
419  gfx::Point location_in_parent(location_in_screen);
420  aura::client::GetScreenPositionClient(windows_.window2->GetRootWindow())->
421      ConvertPointFromScreen(windows_.window2->parent(), &location_in_parent);
422  std::vector<aura::Window*> windows;
423  windows.push_back(windows_.window2);
424  DCHECK(windows_.other_windows.empty());
425  FindWindowsTouching(windows_.window2, windows_.direction,
426                      &windows_.other_windows);
427  for (size_t i = 0; i < windows_.other_windows.size(); ++i) {
428    windows_.other_windows[i]->AddObserver(this);
429    windows.push_back(windows_.other_windows[i]);
430  }
431  int component = windows_.direction == LEFT_RIGHT ? HTRIGHT : HTBOTTOM;
432  wm::WindowState* window_state = wm::GetWindowState(windows_.window1);
433  window_state->CreateDragDetails(windows_.window1,
434                                  location_in_parent,
435                                  component,
436                                  aura::client::WINDOW_MOVE_SOURCE_MOUSE);
437  window_resizer_.reset(WorkspaceWindowResizer::Create(window_state, windows));
438}
439
440void MultiWindowResizeController::Resize(const gfx::Point& location_in_screen,
441                                         int event_flags) {
442  gfx::Point location_in_parent(location_in_screen);
443  aura::client::GetScreenPositionClient(windows_.window1->GetRootWindow())->
444      ConvertPointFromScreen(windows_.window1->parent(), &location_in_parent);
445  window_resizer_->Drag(location_in_parent, event_flags);
446  gfx::Rect bounds = ScreenUtil::ConvertRectToScreen(
447      windows_.window1->parent(),
448      CalculateResizeWidgetBounds(location_in_parent));
449
450  if (windows_.direction == LEFT_RIGHT)
451    bounds.set_y(show_bounds_in_screen_.y());
452  else
453    bounds.set_x(show_bounds_in_screen_.x());
454  resize_widget_->SetBounds(bounds);
455}
456
457void MultiWindowResizeController::CompleteResize() {
458  window_resizer_->CompleteDrag();
459  wm::GetWindowState(window_resizer_->GetTarget())->DeleteDragDetails();
460  window_resizer_.reset();
461
462  // Mouse may still be over resizer, if not hide.
463  gfx::Point screen_loc = Shell::GetScreen()->GetCursorScreenPoint();
464  if (!resize_widget_->GetWindowBoundsInScreen().Contains(screen_loc)) {
465    Hide();
466  } else {
467    // If the mouse is over the resizer we need to remove observers on any of
468    // the |other_windows|. If we start another resize we'll recalculate the
469    // |other_windows| and invoke AddObserver() as necessary.
470    for (size_t i = 0; i < windows_.other_windows.size(); ++i)
471      windows_.other_windows[i]->RemoveObserver(this);
472    windows_.other_windows.clear();
473  }
474}
475
476void MultiWindowResizeController::CancelResize() {
477  if (!window_resizer_)
478    return;  // Happens if window was destroyed and we nuked the WindowResizer.
479  window_resizer_->RevertDrag();
480  wm::GetWindowState(window_resizer_->GetTarget())->DeleteDragDetails();
481  window_resizer_.reset();
482  Hide();
483}
484
485gfx::Rect MultiWindowResizeController::CalculateResizeWidgetBounds(
486    const gfx::Point& location_in_parent) const {
487  gfx::Size pref = resize_widget_->GetContentsView()->GetPreferredSize();
488  int x = 0, y = 0;
489  if (windows_.direction == LEFT_RIGHT) {
490    x = windows_.window1->bounds().right() - pref.width() / 2;
491    y = location_in_parent.y() + kResizeWidgetPadding;
492    if (y + pref.height() / 2 > windows_.window1->bounds().bottom() &&
493        y + pref.height() / 2 > windows_.window2->bounds().bottom()) {
494      y = location_in_parent.y() - kResizeWidgetPadding - pref.height();
495    }
496  } else {
497    x = location_in_parent.x() + kResizeWidgetPadding;
498    if (x + pref.height() / 2 > windows_.window1->bounds().right() &&
499        x + pref.height() / 2 > windows_.window2->bounds().right()) {
500      x = location_in_parent.x() - kResizeWidgetPadding - pref.width();
501    }
502    y = windows_.window1->bounds().bottom() - pref.height() / 2;
503  }
504  return gfx::Rect(x, y, pref.width(), pref.height());
505}
506
507bool MultiWindowResizeController::IsOverWindows(
508    const gfx::Point& location_in_screen) const {
509  if (window_resizer_)
510    return true;  // Ignore hides while actively resizing.
511
512  if (resize_widget_->GetWindowBoundsInScreen().Contains(location_in_screen))
513    return true;
514
515  int hit1, hit2;
516  if (windows_.direction == TOP_BOTTOM) {
517    hit1 = HTBOTTOM;
518    hit2 = HTTOP;
519  } else {
520    hit1 = HTRIGHT;
521    hit2 = HTLEFT;
522  }
523
524  return IsOverWindow(windows_.window1, location_in_screen, hit1) ||
525      IsOverWindow(windows_.window2, location_in_screen, hit2);
526}
527
528bool MultiWindowResizeController::IsOverWindow(
529    aura::Window* window,
530    const gfx::Point& location_in_screen,
531    int component) const {
532  if (!window->delegate())
533    return false;
534
535  gfx::Point window_loc(location_in_screen);
536  aura::Window::ConvertPointToTarget(
537      window->GetRootWindow(), window, &window_loc);
538  return window->ContainsPoint(window_loc) &&
539      window->delegate()->GetNonClientComponent(window_loc) == component;
540}
541
542}  // namespace ash
543