1// Copyright 2014 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/overview/window_grid.h"
6
7#include "ash/ash_switches.h"
8#include "ash/screen_util.h"
9#include "ash/shell.h"
10#include "ash/shell_window_ids.h"
11#include "ash/wm/overview/scoped_transform_overview_window.h"
12#include "ash/wm/overview/window_selector.h"
13#include "ash/wm/overview/window_selector_item.h"
14#include "ash/wm/overview/window_selector_panels.h"
15#include "ash/wm/overview/window_selector_window.h"
16#include "ash/wm/window_state.h"
17#include "base/command_line.h"
18#include "base/i18n/string_search.h"
19#include "base/memory/scoped_vector.h"
20#include "third_party/skia/include/core/SkColor.h"
21#include "ui/aura/window.h"
22#include "ui/compositor/layer_animation_observer.h"
23#include "ui/compositor/scoped_layer_animation_settings.h"
24#include "ui/gfx/animation/tween.h"
25#include "ui/gfx/vector2d.h"
26#include "ui/views/background.h"
27#include "ui/views/view.h"
28#include "ui/views/widget/widget.h"
29#include "ui/wm/core/window_animations.h"
30
31namespace ash {
32namespace {
33
34// An observer which holds onto the passed widget until the animation is
35// complete.
36class CleanupWidgetAfterAnimationObserver
37    : public ui::ImplicitAnimationObserver {
38 public:
39  explicit CleanupWidgetAfterAnimationObserver(
40      scoped_ptr<views::Widget> widget);
41  virtual ~CleanupWidgetAfterAnimationObserver();
42
43  // ui::ImplicitAnimationObserver:
44  virtual void OnImplicitAnimationsCompleted() OVERRIDE;
45
46 private:
47  scoped_ptr<views::Widget> widget_;
48
49  DISALLOW_COPY_AND_ASSIGN(CleanupWidgetAfterAnimationObserver);
50};
51
52CleanupWidgetAfterAnimationObserver::CleanupWidgetAfterAnimationObserver(
53    scoped_ptr<views::Widget> widget)
54    : widget_(widget.Pass()) {
55}
56
57CleanupWidgetAfterAnimationObserver::~CleanupWidgetAfterAnimationObserver() {
58}
59
60void CleanupWidgetAfterAnimationObserver::OnImplicitAnimationsCompleted() {
61  delete this;
62}
63
64// A comparator for locating a given target window.
65struct WindowSelectorItemComparator
66    : public std::unary_function<WindowSelectorItem*, bool> {
67  explicit WindowSelectorItemComparator(const aura::Window* target_window)
68      : target(target_window) {
69  }
70
71  bool operator()(WindowSelectorItem* window) const {
72    return window->HasSelectableWindow(target);
73  }
74
75  const aura::Window* target;
76};
77
78// A comparator for locating a WindowSelectorItem given a targeted window.
79struct WindowSelectorItemTargetComparator
80    : public std::unary_function<WindowSelectorItem*, bool> {
81  explicit WindowSelectorItemTargetComparator(const aura::Window* target_window)
82      : target(target_window) {
83  }
84
85  bool operator()(WindowSelectorItem* window) const {
86    return window->Contains(target);
87  }
88
89  const aura::Window* target;
90};
91
92// Conceptually the window overview is a table or grid of cells having this
93// fixed aspect ratio. The number of columns is determined by maximizing the
94// area of them based on the number of window_list.
95const float kCardAspectRatio = 4.0f / 3.0f;
96
97// The minimum number of cards along the major axis (i.e. horizontally on a
98// landscape orientation).
99const int kMinCardsMajor = 3;
100
101const int kOverviewSelectorTransitionMilliseconds = 100;
102
103// The color and opacity of the overview selector.
104const SkColor kWindowOverviewSelectionColor = SK_ColorBLACK;
105const unsigned char kWindowOverviewSelectorOpacity = 128;
106
107// The minimum amount of spacing between the bottom of the text filtering
108// text field and the top of the selection widget on the first row of items.
109const int kTextFilterBottomMargin = 5;
110
111// Returns the vector for the fade in animation.
112gfx::Vector2d GetSlideVectorForFadeIn(WindowSelector::Direction direction,
113                                      const gfx::Rect& bounds) {
114  gfx::Vector2d vector;
115  switch (direction) {
116    case WindowSelector::DOWN:
117      vector.set_y(bounds.width());
118      break;
119    case WindowSelector::RIGHT:
120      vector.set_x(bounds.height());
121      break;
122    case WindowSelector::UP:
123      vector.set_y(-bounds.width());
124      break;
125    case WindowSelector::LEFT:
126      vector.set_x(-bounds.height());
127      break;
128  }
129  return vector;
130}
131
132}  // namespace
133
134WindowGrid::WindowGrid(aura::Window* root_window,
135                       const std::vector<aura::Window*>& windows,
136                       WindowSelector* window_selector)
137    : root_window_(root_window),
138      window_selector_(window_selector) {
139  WindowSelectorPanels* panels_item = NULL;
140  for (aura::Window::Windows::const_iterator iter = windows.begin();
141       iter != windows.end(); ++iter) {
142    if ((*iter)->GetRootWindow() != root_window)
143      continue;
144    (*iter)->AddObserver(this);
145    observed_windows_.insert(*iter);
146
147    if ((*iter)->type() == ui::wm::WINDOW_TYPE_PANEL &&
148        wm::GetWindowState(*iter)->panel_attached()) {
149      // Attached panel windows are grouped into a single overview item per
150      // grid.
151      if (!panels_item) {
152        panels_item = new WindowSelectorPanels(root_window_);
153        window_list_.push_back(panels_item);
154      }
155      panels_item->AddWindow(*iter);
156    } else {
157      window_list_.push_back(new WindowSelectorWindow(*iter));
158    }
159  }
160  if (window_list_.empty())
161    return;
162}
163
164WindowGrid::~WindowGrid() {
165  for (std::set<aura::Window*>::iterator iter = observed_windows_.begin();
166       iter != observed_windows_.end(); iter++) {
167    (*iter)->RemoveObserver(this);
168  }
169}
170
171void WindowGrid::PrepareForOverview() {
172  for (ScopedVector<WindowSelectorItem>::iterator iter = window_list_.begin();
173       iter != window_list_.end(); ++iter) {
174    (*iter)->PrepareForOverview();
175  }
176}
177
178void WindowGrid::PositionWindows(bool animate) {
179  CHECK(!window_list_.empty());
180
181  gfx::Size window_size;
182  gfx::Rect total_bounds = ScreenUtil::ConvertRectToScreen(
183      root_window_,
184      ScreenUtil::GetDisplayWorkAreaBoundsInParent(
185          Shell::GetContainer(root_window_, kShellWindowId_DefaultContainer)));
186
187  // If the text filtering feature is enabled, reserve space at the top for the
188  // text filtering textbox to appear.
189  if (!CommandLine::ForCurrentProcess()->HasSwitch(
190          switches::kAshDisableTextFilteringInOverviewMode)) {
191    total_bounds.Inset(
192        0,
193        WindowSelector::kTextFilterBottomEdge + kTextFilterBottomMargin,
194        0,
195        0);
196  }
197
198  // Find the minimum number of windows per row that will fit all of the
199  // windows on screen.
200  num_columns_ = std::max(
201      total_bounds.width() > total_bounds.height() ? kMinCardsMajor : 1,
202      static_cast<int>(ceil(sqrt(total_bounds.width() * window_list_.size() /
203                                 (kCardAspectRatio * total_bounds.height())))));
204  int num_rows = ((window_list_.size() + num_columns_ - 1) / num_columns_);
205  window_size.set_width(std::min(
206      static_cast<int>(total_bounds.width() / num_columns_),
207      static_cast<int>(total_bounds.height() * kCardAspectRatio / num_rows)));
208  window_size.set_height(window_size.width() / kCardAspectRatio);
209
210  // Calculate the X and Y offsets necessary to center the grid.
211  int x_offset = total_bounds.x() + ((window_list_.size() >= num_columns_ ? 0 :
212      (num_columns_ - window_list_.size()) * window_size.width()) +
213      (total_bounds.width() - num_columns_ * window_size.width())) / 2;
214  int y_offset = total_bounds.y() + (total_bounds.height() -
215      num_rows * window_size.height()) / 2;
216
217  for (size_t i = 0; i < window_list_.size(); ++i) {
218    gfx::Transform transform;
219    int column = i % num_columns_;
220    int row = i / num_columns_;
221    gfx::Rect target_bounds(window_size.width() * column + x_offset,
222                            window_size.height() * row + y_offset,
223                            window_size.width(),
224                            window_size.height());
225    window_list_[i]->SetBounds(root_window_, target_bounds, animate);
226  }
227
228  // If we have less than |kMinCardsMajor| windows, adjust the column_ value to
229  // reflect how many "real" columns we have.
230  if (num_columns_ > window_list_.size())
231    num_columns_ = window_list_.size();
232
233  // If the selection widget is active, reposition it without any animation.
234  if (selection_widget_)
235    MoveSelectionWidgetToTarget(animate);
236}
237
238bool WindowGrid::Move(WindowSelector::Direction direction, bool animate) {
239  bool recreate_selection_widget = false;
240  bool out_of_bounds = false;
241  if (!selection_widget_) {
242    switch (direction) {
243     case WindowSelector::LEFT:
244       selected_index_ = window_list_.size() - 1;
245       break;
246     case WindowSelector::UP:
247       selected_index_ =
248           (window_list_.size() / num_columns_) * num_columns_ - 1;
249       break;
250     case WindowSelector::RIGHT:
251     case WindowSelector::DOWN:
252       selected_index_ = 0;
253       break;
254     }
255  }
256  while (SelectedWindow()->dimmed() || selection_widget_) {
257    switch (direction) {
258      case WindowSelector::RIGHT:
259        if (selected_index_ >= window_list_.size() - 1)
260          out_of_bounds = true;
261        selected_index_++;
262        if (selected_index_ % num_columns_ == 0)
263          recreate_selection_widget = true;
264        break;
265      case WindowSelector::LEFT:
266        if (selected_index_ == 0)
267          out_of_bounds = true;
268        selected_index_--;
269        if ((selected_index_ + 1) % num_columns_ == 0)
270          recreate_selection_widget = true;
271        break;
272      case WindowSelector::DOWN:
273        selected_index_ += num_columns_;
274        if (selected_index_ >= window_list_.size()) {
275          selected_index_ = (selected_index_ + 1) % num_columns_;
276          if (selected_index_ == 0)
277            out_of_bounds = true;
278          recreate_selection_widget = true;
279        }
280        break;
281      case WindowSelector::UP:
282        if (selected_index_ == 0)
283          out_of_bounds = true;
284        if (selected_index_ < num_columns_) {
285          selected_index_ += num_columns_ *
286              ((window_list_.size() - selected_index_) / num_columns_) - 1;
287          recreate_selection_widget = true;
288        } else {
289          selected_index_ -= num_columns_;
290        }
291        break;
292    }
293    // Exit the loop if we broke free from the grid or found an active item.
294    if (out_of_bounds || !SelectedWindow()->dimmed())
295      break;
296  }
297
298  MoveSelectionWidget(direction, recreate_selection_widget,
299                      out_of_bounds, animate);
300  return out_of_bounds;
301}
302
303WindowSelectorItem* WindowGrid::SelectedWindow() const {
304  CHECK(selected_index_ < window_list_.size());
305  return window_list_[selected_index_];
306}
307
308bool WindowGrid::Contains(const aura::Window* window) const {
309  return std::find_if(window_list_.begin(), window_list_.end(),
310                      WindowSelectorItemTargetComparator(window)) !=
311                          window_list_.end();
312}
313
314void WindowGrid::FilterItems(const base::string16& pattern) {
315  base::i18n::FixedPatternStringSearchIgnoringCaseAndAccents finder(pattern);
316  for (ScopedVector<WindowSelectorItem>::iterator iter = window_list_.begin();
317       iter != window_list_.end(); iter++) {
318    if (finder.Search((*iter)->SelectionWindow()->title(), NULL, NULL)) {
319      (*iter)->SetDimmed(false);
320    } else {
321      (*iter)->SetDimmed(true);
322      if (selection_widget_ && SelectedWindow() == *iter)
323        selection_widget_.reset();
324    }
325  }
326}
327
328void WindowGrid::OnWindowDestroying(aura::Window* window) {
329  window->RemoveObserver(this);
330  observed_windows_.erase(window);
331  ScopedVector<WindowSelectorItem>::iterator iter =
332      std::find_if(window_list_.begin(), window_list_.end(),
333                   WindowSelectorItemComparator(window));
334
335  DCHECK(iter != window_list_.end());
336
337  (*iter)->RemoveWindow(window);
338
339  // If there are still windows in this selector entry then the overview is
340  // still active and the active selection remains the same.
341  if (!(*iter)->empty())
342    return;
343
344  size_t removed_index = iter - window_list_.begin();
345  window_list_.erase(iter);
346
347  if (empty()) {
348    // If the grid is now empty, notify the window selector so that it erases us
349    // from its grid list.
350    window_selector_->OnGridEmpty(this);
351    return;
352  }
353
354  // If selecting, update the selection index.
355  if (selection_widget_) {
356    bool send_focus_alert = selected_index_ == removed_index;
357    if (selected_index_ >= removed_index && selected_index_ != 0)
358      selected_index_--;
359    if (send_focus_alert)
360      SelectedWindow()->SendFocusAlert();
361  }
362
363  PositionWindows(true);
364}
365
366void WindowGrid::OnWindowBoundsChanged(aura::Window* window,
367                                       const gfx::Rect& old_bounds,
368                                       const gfx::Rect& new_bounds) {
369  ScopedVector<WindowSelectorItem>::const_iterator iter =
370      std::find_if(window_list_.begin(), window_list_.end(),
371                   WindowSelectorItemTargetComparator(window));
372  DCHECK(iter != window_list_.end());
373
374  // Immediately finish any active bounds animation.
375  window->layer()->GetAnimator()->StopAnimatingProperty(
376      ui::LayerAnimationElement::BOUNDS);
377
378  // Recompute the transform for the window.
379  (*iter)->RecomputeWindowTransforms();
380}
381
382void WindowGrid::InitSelectionWidget(WindowSelector::Direction direction) {
383  selection_widget_.reset(new views::Widget);
384  views::Widget::InitParams params;
385  params.type = views::Widget::InitParams::TYPE_POPUP;
386  params.keep_on_top = false;
387  params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
388  params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
389  params.parent = Shell::GetContainer(root_window_,
390                                      kShellWindowId_DefaultContainer);
391  params.accept_events = false;
392  selection_widget_->set_focus_on_creation(false);
393  selection_widget_->Init(params);
394  // Disable the "bounce in" animation when showing the window.
395  ::wm::SetWindowVisibilityAnimationTransition(
396      selection_widget_->GetNativeWindow(), ::wm::ANIMATE_NONE);
397  // The selection widget should not activate the shelf when passing under it.
398  ash::wm::GetWindowState(selection_widget_->GetNativeWindow())->
399      set_ignored_by_shelf(true);
400
401  views::View* content_view = new views::View;
402  content_view->set_background(
403      views::Background::CreateSolidBackground(kWindowOverviewSelectionColor));
404  selection_widget_->SetContentsView(content_view);
405  selection_widget_->GetNativeWindow()->parent()->StackChildAtBottom(
406      selection_widget_->GetNativeWindow());
407  selection_widget_->Show();
408  // New selection widget starts with 0 opacity and then fades in.
409  selection_widget_->GetNativeWindow()->layer()->SetOpacity(0);
410
411  const gfx::Rect target_bounds = SelectedWindow()->target_bounds();
412  gfx::Vector2d fade_out_direction =
413          GetSlideVectorForFadeIn(direction, target_bounds);
414  gfx::Display dst_display = gfx::Screen::GetScreenFor(root_window_)->
415      GetDisplayMatching(target_bounds);
416  selection_widget_->GetNativeWindow()->SetBoundsInScreen(
417      target_bounds - fade_out_direction, dst_display);
418}
419
420void WindowGrid::MoveSelectionWidget(WindowSelector::Direction direction,
421                                     bool recreate_selection_widget,
422                                     bool out_of_bounds,
423                                     bool animate) {
424  // If the selection widget is already active, fade it out in the selection
425  // direction.
426  if (selection_widget_ && (recreate_selection_widget || out_of_bounds)) {
427    // Animate the old selection widget and then destroy it.
428    views::Widget* old_selection = selection_widget_.get();
429    gfx::Vector2d fade_out_direction =
430        GetSlideVectorForFadeIn(
431            direction, old_selection->GetNativeWindow()->bounds());
432
433    ui::ScopedLayerAnimationSettings animation_settings(
434        old_selection->GetNativeWindow()->layer()->GetAnimator());
435    animation_settings.SetTransitionDuration(
436        base::TimeDelta::FromMilliseconds(
437            kOverviewSelectorTransitionMilliseconds));
438    animation_settings.SetPreemptionStrategy(
439        ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
440    animation_settings.SetTweenType(gfx::Tween::FAST_OUT_LINEAR_IN);
441    // CleanupWidgetAfterAnimationObserver will delete itself (and the
442    // widget) when the movement animation is complete.
443    animation_settings.AddObserver(
444        new CleanupWidgetAfterAnimationObserver(selection_widget_.Pass()));
445    old_selection->SetOpacity(0);
446    old_selection->GetNativeWindow()->SetBounds(
447        old_selection->GetNativeWindow()->bounds() + fade_out_direction);
448    old_selection->Hide();
449  }
450  if (out_of_bounds)
451    return;
452
453  if (!selection_widget_)
454    InitSelectionWidget(direction);
455  // Send an a11y alert so that if ChromeVox is enabled, the item label is
456  // read.
457  SelectedWindow()->SendFocusAlert();
458  // The selection widget is moved to the newly selected item in the same
459  // grid.
460  MoveSelectionWidgetToTarget(animate);
461}
462
463void WindowGrid::MoveSelectionWidgetToTarget(bool animate) {
464  if (animate) {
465    ui::ScopedLayerAnimationSettings animation_settings(
466        selection_widget_->GetNativeWindow()->layer()->GetAnimator());
467    animation_settings.SetTransitionDuration(base::TimeDelta::FromMilliseconds(
468        kOverviewSelectorTransitionMilliseconds));
469    animation_settings.SetTweenType(gfx::Tween::LINEAR_OUT_SLOW_IN);
470    animation_settings.SetPreemptionStrategy(
471        ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
472    selection_widget_->SetBounds(SelectedWindow()->target_bounds());
473    selection_widget_->SetOpacity(kWindowOverviewSelectorOpacity);
474    return;
475  }
476  selection_widget_->SetBounds(SelectedWindow()->target_bounds());
477  selection_widget_->SetOpacity(kWindowOverviewSelectorOpacity);
478}
479
480}  // namespace ash
481