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