1// Copyright 2013 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_selector.h" 6 7#include <algorithm> 8 9#include "ash/accessibility_delegate.h" 10#include "ash/ash_switches.h" 11#include "ash/metrics/user_metrics_recorder.h" 12#include "ash/root_window_controller.h" 13#include "ash/shell.h" 14#include "ash/shell_window_ids.h" 15#include "ash/switchable_windows.h" 16#include "ash/wm/overview/scoped_transform_overview_window.h" 17#include "ash/wm/overview/window_grid.h" 18#include "ash/wm/overview/window_selector_delegate.h" 19#include "ash/wm/overview/window_selector_item.h" 20#include "ash/wm/window_state.h" 21#include "base/auto_reset.h" 22#include "base/command_line.h" 23#include "base/metrics/histogram.h" 24#include "third_party/skia/include/core/SkPaint.h" 25#include "third_party/skia/include/core/SkPath.h" 26#include "ui/aura/client/focus_client.h" 27#include "ui/aura/window.h" 28#include "ui/aura/window_event_dispatcher.h" 29#include "ui/aura/window_observer.h" 30#include "ui/base/resource/resource_bundle.h" 31#include "ui/compositor/scoped_layer_animation_settings.h" 32#include "ui/events/event.h" 33#include "ui/gfx/canvas.h" 34#include "ui/gfx/screen.h" 35#include "ui/gfx/skia_util.h" 36#include "ui/views/border.h" 37#include "ui/views/controls/textfield/textfield.h" 38#include "ui/views/layout/box_layout.h" 39#include "ui/wm/core/window_util.h" 40#include "ui/wm/public/activation_client.h" 41 42namespace ash { 43 44namespace { 45 46// The proportion of screen width that the text filter takes. 47const float kTextFilterScreenProportion = 0.25; 48 49// The amount of padding surrounding the text in the text filtering textbox. 50const int kTextFilterHorizontalPadding = 8; 51 52// The distance between the top of the screen and the top edge of the 53// text filtering textbox. 54const int kTextFilterDistanceFromTop = 32; 55 56// The height of the text filtering textbox. 57const int kTextFilterHeight = 32; 58 59// The font style used for text filtering. 60static const ::ui::ResourceBundle::FontStyle kTextFilterFontStyle = 61 ::ui::ResourceBundle::FontStyle::MediumFont; 62 63// The alpha value for the background of the text filtering textbox. 64const unsigned char kTextFilterOpacity = 180; 65 66// The radius used for the rounded corners on the text filtering textbox. 67const int kTextFilterCornerRadius = 1; 68 69// A comparator for locating a grid with a given root window. 70struct RootWindowGridComparator 71 : public std::unary_function<WindowGrid*, bool> { 72 explicit RootWindowGridComparator(const aura::Window* root_window) 73 : root_window_(root_window) { 74 } 75 76 bool operator()(WindowGrid* grid) const { 77 return (grid->root_window() == root_window_); 78 } 79 80 const aura::Window* root_window_; 81}; 82 83// A comparator for locating a selectable window given a targeted window. 84struct WindowSelectorItemTargetComparator 85 : public std::unary_function<WindowSelectorItem*, bool> { 86 explicit WindowSelectorItemTargetComparator(const aura::Window* target_window) 87 : target(target_window) { 88 } 89 90 bool operator()(WindowSelectorItem* window) const { 91 return window->Contains(target); 92 } 93 94 const aura::Window* target; 95}; 96 97// A comparator for locating a selector item for a given root. 98struct WindowSelectorItemForRoot 99 : public std::unary_function<WindowSelectorItem*, bool> { 100 explicit WindowSelectorItemForRoot(const aura::Window* root) 101 : root_window(root) { 102 } 103 104 bool operator()(WindowSelectorItem* item) const { 105 return item->GetRootWindow() == root_window; 106 } 107 108 const aura::Window* root_window; 109}; 110 111// A View having rounded corners and a specified background color which is 112// only painted within the bounds defined by the rounded corners. 113// TODO(tdanderson): This duplicates code from RoundedImageView. Refactor these 114// classes and move into ui/views. 115class RoundedContainerView : public views::View { 116 public: 117 RoundedContainerView(int corner_radius, SkColor background) 118 : corner_radius_(corner_radius), 119 background_(background) { 120 } 121 122 virtual ~RoundedContainerView() {} 123 124 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE { 125 views::View::OnPaint(canvas); 126 127 SkScalar radius = SkIntToScalar(corner_radius_); 128 const SkScalar kRadius[8] = {radius, radius, radius, radius, 129 radius, radius, radius, radius}; 130 SkPath path; 131 gfx::Rect bounds(size()); 132 path.addRoundRect(gfx::RectToSkRect(bounds), kRadius); 133 134 SkPaint paint; 135 paint.setAntiAlias(true); 136 canvas->ClipPath(path, true); 137 canvas->DrawColor(background_); 138 } 139 140 private: 141 int corner_radius_; 142 SkColor background_; 143 144 DISALLOW_COPY_AND_ASSIGN(RoundedContainerView); 145}; 146 147// Triggers a shelf visibility update on all root window controllers. 148void UpdateShelfVisibility() { 149 Shell::RootWindowControllerList root_window_controllers = 150 Shell::GetInstance()->GetAllRootWindowControllers(); 151 for (Shell::RootWindowControllerList::iterator iter = 152 root_window_controllers.begin(); 153 iter != root_window_controllers.end(); ++iter) { 154 (*iter)->UpdateShelfVisibility(); 155 } 156} 157 158// Initializes the text filter on the top of the main root window and requests 159// focus on its textfield. 160views::Widget* CreateTextFilter(views::TextfieldController* controller, 161 aura::Window* root_window) { 162 views::Widget* widget = new views::Widget; 163 views::Widget::InitParams params; 164 params.type = views::Widget::InitParams::TYPE_WINDOW_FRAMELESS; 165 params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; 166 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; 167 params.parent = 168 Shell::GetContainer(root_window, ash::kShellWindowId_OverlayContainer); 169 params.accept_events = true; 170 params.bounds = gfx::Rect( 171 root_window->bounds().width() / 2 * (1 - kTextFilterScreenProportion), 172 kTextFilterDistanceFromTop, 173 root_window->bounds().width() * kTextFilterScreenProportion, 174 kTextFilterHeight); 175 widget->Init(params); 176 177 // Use |container| to specify the padding surrounding the text and to give 178 // the textfield rounded corners. 179 views::View* container = new RoundedContainerView( 180 kTextFilterCornerRadius, SkColorSetARGB(kTextFilterOpacity, 0, 0, 0)); 181 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance(); 182 int text_height = bundle.GetFontList(kTextFilterFontStyle).GetHeight(); 183 DCHECK(text_height); 184 int vertical_padding = (kTextFilterHeight - text_height) / 2; 185 container->SetLayoutManager(new views::BoxLayout(views::BoxLayout::kVertical, 186 kTextFilterHorizontalPadding, 187 vertical_padding, 188 0)); 189 190 views::Textfield* textfield = new views::Textfield; 191 textfield->set_controller(controller); 192 textfield->SetBackgroundColor(SK_ColorTRANSPARENT); 193 textfield->SetBorder(views::Border::NullBorder()); 194 textfield->SetTextColor(SK_ColorWHITE); 195 textfield->SetFontList(bundle.GetFontList(kTextFilterFontStyle)); 196 197 container->AddChildView(textfield); 198 widget->SetContentsView(container); 199 200 // The textfield initially contains no text, so shift its position to be 201 // outside the visible bounds of the screen. 202 gfx::Transform transform; 203 transform.Translate(0, -WindowSelector::kTextFilterBottomEdge); 204 widget->GetNativeWindow()->SetTransform(transform); 205 widget->Show(); 206 textfield->RequestFocus(); 207 208 return widget; 209} 210 211} // namespace 212 213const int WindowSelector::kTextFilterBottomEdge = 214 kTextFilterDistanceFromTop + kTextFilterHeight; 215 216WindowSelector::WindowSelector(const WindowList& windows, 217 WindowSelectorDelegate* delegate) 218 : delegate_(delegate), 219 restore_focus_window_(aura::client::GetFocusClient( 220 Shell::GetPrimaryRootWindow())->GetFocusedWindow()), 221 ignore_activations_(false), 222 selected_grid_index_(0), 223 overview_start_time_(base::Time::Now()), 224 num_key_presses_(0), 225 num_items_(0), 226 showing_selection_widget_(false), 227 text_filter_string_length_(0), 228 num_times_textfield_cleared_(0) { 229 DCHECK(delegate_); 230 Shell* shell = Shell::GetInstance(); 231 shell->OnOverviewModeStarting(); 232 233 if (restore_focus_window_) 234 restore_focus_window_->AddObserver(this); 235 236 const aura::Window::Windows root_windows = Shell::GetAllRootWindows(); 237 for (aura::Window::Windows::const_iterator iter = root_windows.begin(); 238 iter != root_windows.end(); iter++) { 239 // Observed switchable containers for newly created windows on all root 240 // windows. 241 for (size_t i = 0; i < kSwitchableWindowContainerIdsLength; ++i) { 242 aura::Window* container = Shell::GetContainer(*iter, 243 kSwitchableWindowContainerIds[i]); 244 container->AddObserver(this); 245 observed_windows_.insert(container); 246 } 247 scoped_ptr<WindowGrid> grid(new WindowGrid(*iter, windows, this)); 248 if (grid->empty()) 249 continue; 250 num_items_ += grid->size(); 251 grid_list_.push_back(grid.release()); 252 } 253 254 // Do not call PrepareForOverview until all items are added to window_list_ as 255 // we don't want to cause any window updates until all windows in overview 256 // are observed. See http://crbug.com/384495. 257 for (ScopedVector<WindowGrid>::iterator iter = grid_list_.begin(); 258 iter != grid_list_.end(); ++iter) { 259 (*iter)->PrepareForOverview(); 260 (*iter)->PositionWindows(true); 261 } 262 263 DCHECK(!grid_list_.empty()); 264 UMA_HISTOGRAM_COUNTS_100("Ash.WindowSelector.Items", num_items_); 265 266 text_filter_widget_.reset( 267 CreateTextFilter(this, Shell::GetPrimaryRootWindow())); 268 269 shell->activation_client()->AddObserver(this); 270 271 shell->GetScreen()->AddObserver(this); 272 shell->metrics()->RecordUserMetricsAction(UMA_WINDOW_OVERVIEW); 273 HideAndTrackNonOverviewWindows(); 274 // Send an a11y alert. 275 shell->accessibility_delegate()->TriggerAccessibilityAlert( 276 A11Y_ALERT_WINDOW_OVERVIEW_MODE_ENTERED); 277 278 UpdateShelfVisibility(); 279} 280 281WindowSelector::~WindowSelector() { 282 ash::Shell* shell = ash::Shell::GetInstance(); 283 284 ResetFocusRestoreWindow(true); 285 for (std::set<aura::Window*>::iterator iter = observed_windows_.begin(); 286 iter != observed_windows_.end(); ++iter) { 287 (*iter)->RemoveObserver(this); 288 } 289 shell->activation_client()->RemoveObserver(this); 290 aura::Window::Windows root_windows = Shell::GetAllRootWindows(); 291 292 const aura::WindowTracker::Windows hidden_windows(hidden_windows_.windows()); 293 for (aura::WindowTracker::Windows::const_iterator iter = 294 hidden_windows.begin(); iter != hidden_windows.end(); ++iter) { 295 ui::ScopedLayerAnimationSettings settings( 296 (*iter)->layer()->GetAnimator()); 297 settings.SetTransitionDuration(base::TimeDelta::FromMilliseconds( 298 ScopedTransformOverviewWindow::kTransitionMilliseconds)); 299 settings.SetPreemptionStrategy( 300 ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); 301 (*iter)->layer()->SetOpacity(1); 302 (*iter)->Show(); 303 } 304 305 shell->GetScreen()->RemoveObserver(this); 306 307 size_t remaining_items = 0; 308 for (ScopedVector<WindowGrid>::iterator iter = grid_list_.begin(); 309 iter != grid_list_.end(); iter++) { 310 remaining_items += (*iter)->size(); 311 } 312 313 DCHECK(num_items_ >= remaining_items); 314 UMA_HISTOGRAM_COUNTS_100("Ash.WindowSelector.OverviewClosedItems", 315 num_items_ - remaining_items); 316 UMA_HISTOGRAM_MEDIUM_TIMES("Ash.WindowSelector.TimeInOverview", 317 base::Time::Now() - overview_start_time_); 318 319 // Record metrics related to text filtering. 320 UMA_HISTOGRAM_COUNTS_100("Ash.WindowSelector.TextFilteringStringLength", 321 text_filter_string_length_); 322 UMA_HISTOGRAM_COUNTS_100("Ash.WindowSelector.TextFilteringTextfieldCleared", 323 num_times_textfield_cleared_); 324 if (text_filter_string_length_) { 325 UMA_HISTOGRAM_MEDIUM_TIMES( 326 "Ash.WindowSelector.TimeInOverviewWithTextFiltering", 327 base::Time::Now() - overview_start_time_); 328 UMA_HISTOGRAM_COUNTS_100( 329 "Ash.WindowSelector.ItemsWhenTextFilteringUsed", 330 remaining_items); 331 } 332 333 // TODO(flackr): Change this to OnOverviewModeEnded and move it to when 334 // everything is done. 335 shell->OnOverviewModeEnding(); 336 337 // Clearing the window list resets the ignored_by_shelf flag on the windows. 338 grid_list_.clear(); 339 UpdateShelfVisibility(); 340} 341 342void WindowSelector::CancelSelection() { 343 delegate_->OnSelectionEnded(); 344} 345 346void WindowSelector::OnGridEmpty(WindowGrid* grid) { 347 ScopedVector<WindowGrid>::iterator iter = 348 std::find(grid_list_.begin(), grid_list_.end(), grid); 349 DCHECK(iter != grid_list_.end()); 350 grid_list_.erase(iter); 351 // TODO(flackr): Use the previous index for more than two displays. 352 selected_grid_index_ = 0; 353 if (grid_list_.empty()) 354 CancelSelection(); 355} 356 357bool WindowSelector::HandleKeyEvent(views::Textfield* sender, 358 const ui::KeyEvent& key_event) { 359 if (key_event.type() != ui::ET_KEY_PRESSED) 360 return false; 361 362 switch (key_event.key_code()) { 363 case ui::VKEY_ESCAPE: 364 CancelSelection(); 365 break; 366 case ui::VKEY_UP: 367 num_key_presses_++; 368 Move(WindowSelector::UP, true); 369 break; 370 case ui::VKEY_DOWN: 371 num_key_presses_++; 372 Move(WindowSelector::DOWN, true); 373 break; 374 case ui::VKEY_RIGHT: 375 case ui::VKEY_TAB: 376 num_key_presses_++; 377 Move(WindowSelector::RIGHT, true); 378 break; 379 case ui::VKEY_LEFT: 380 num_key_presses_++; 381 Move(WindowSelector::LEFT, true); 382 break; 383 case ui::VKEY_RETURN: 384 // Ignore if no item is selected. 385 if (!grid_list_[selected_grid_index_]->is_selecting()) 386 return false; 387 UMA_HISTOGRAM_COUNTS_100("Ash.WindowSelector.ArrowKeyPresses", 388 num_key_presses_); 389 UMA_HISTOGRAM_CUSTOM_COUNTS( 390 "Ash.WindowSelector.KeyPressesOverItemsRatio", 391 (num_key_presses_ * 100) / num_items_, 1, 300, 30); 392 Shell::GetInstance()->metrics()->RecordUserMetricsAction( 393 UMA_WINDOW_OVERVIEW_ENTER_KEY); 394 wm::GetWindowState(grid_list_[selected_grid_index_]-> 395 SelectedWindow()->SelectionWindow())->Activate(); 396 break; 397 default: 398 // Not a key we are interested in, allow the textfield to handle it. 399 return false; 400 } 401 return true; 402} 403 404void WindowSelector::OnDisplayAdded(const gfx::Display& display) { 405} 406 407void WindowSelector::OnDisplayRemoved(const gfx::Display& display) { 408 // TODO(flackr): Keep window selection active on remaining displays. 409 CancelSelection(); 410} 411 412void WindowSelector::OnDisplayMetricsChanged(const gfx::Display& display, 413 uint32_t metrics) { 414 PositionWindows(/* animate */ false); 415} 416 417void WindowSelector::OnWindowAdded(aura::Window* new_window) { 418 if (new_window->type() != ui::wm::WINDOW_TYPE_NORMAL && 419 new_window->type() != ui::wm::WINDOW_TYPE_PANEL) { 420 return; 421 } 422 423 for (size_t i = 0; i < kSwitchableWindowContainerIdsLength; ++i) { 424 if (new_window->parent()->id() == kSwitchableWindowContainerIds[i] && 425 !::wm::GetTransientParent(new_window)) { 426 // The new window is in one of the switchable containers, abort overview. 427 CancelSelection(); 428 return; 429 } 430 } 431} 432 433void WindowSelector::OnWindowDestroying(aura::Window* window) { 434 window->RemoveObserver(this); 435 observed_windows_.erase(window); 436 if (window == restore_focus_window_) 437 restore_focus_window_ = NULL; 438} 439 440void WindowSelector::OnWindowActivated(aura::Window* gained_active, 441 aura::Window* lost_active) { 442 if (ignore_activations_ || 443 !gained_active || 444 gained_active == text_filter_widget_->GetNativeWindow()) { 445 return; 446 } 447 448 ScopedVector<WindowGrid>::iterator grid = 449 std::find_if(grid_list_.begin(), grid_list_.end(), 450 RootWindowGridComparator(gained_active->GetRootWindow())); 451 if (grid == grid_list_.end()) 452 return; 453 const std::vector<WindowSelectorItem*> windows = (*grid)->window_list(); 454 455 ScopedVector<WindowSelectorItem>::const_iterator iter = std::find_if( 456 windows.begin(), windows.end(), 457 WindowSelectorItemTargetComparator(gained_active)); 458 459 if (iter != windows.end()) 460 (*iter)->RestoreWindowOnExit(gained_active); 461 462 // Don't restore focus on exit if a window was just activated. 463 ResetFocusRestoreWindow(false); 464 CancelSelection(); 465} 466 467void WindowSelector::OnAttemptToReactivateWindow(aura::Window* request_active, 468 aura::Window* actual_active) { 469 OnWindowActivated(request_active, actual_active); 470} 471 472void WindowSelector::ContentsChanged(views::Textfield* sender, 473 const base::string16& new_contents) { 474 if (CommandLine::ForCurrentProcess()->HasSwitch( 475 switches::kAshDisableTextFilteringInOverviewMode)) { 476 return; 477 } 478 479 text_filter_string_length_ = new_contents.length(); 480 if (!text_filter_string_length_) 481 num_times_textfield_cleared_++; 482 483 bool should_show_selection_widget = !new_contents.empty(); 484 if (showing_selection_widget_ != should_show_selection_widget) { 485 ui::ScopedLayerAnimationSettings animation_settings( 486 text_filter_widget_->GetNativeWindow()->layer()->GetAnimator()); 487 animation_settings.SetPreemptionStrategy( 488 ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); 489 animation_settings.SetTweenType(showing_selection_widget_ ? 490 gfx::Tween::FAST_OUT_LINEAR_IN : gfx::Tween::LINEAR_OUT_SLOW_IN); 491 492 gfx::Transform transform; 493 if (should_show_selection_widget) { 494 transform.Translate(0, 0); 495 text_filter_widget_->GetNativeWindow()->layer()->SetOpacity(1); 496 } else { 497 transform.Translate(0, -kTextFilterBottomEdge); 498 text_filter_widget_->GetNativeWindow()->layer()->SetOpacity(0); 499 } 500 501 text_filter_widget_->GetNativeWindow()->SetTransform(transform); 502 showing_selection_widget_ = should_show_selection_widget; 503 } 504 for (ScopedVector<WindowGrid>::iterator iter = grid_list_.begin(); 505 iter != grid_list_.end(); iter++) { 506 (*iter)->FilterItems(new_contents); 507 } 508 509 // If the selection widget is not active, execute a Move() command so that it 510 // shows up on the first undimmed item. 511 if (grid_list_[selected_grid_index_]->is_selecting()) 512 return; 513 Move(WindowSelector::RIGHT, false); 514} 515 516void WindowSelector::PositionWindows(bool animate) { 517 for (ScopedVector<WindowGrid>::iterator iter = grid_list_.begin(); 518 iter != grid_list_.end(); iter++) { 519 (*iter)->PositionWindows(animate); 520 } 521} 522 523void WindowSelector::HideAndTrackNonOverviewWindows() { 524 // Add the windows to hidden_windows first so that if any are destroyed 525 // while hiding them they are tracked. 526 for (ScopedVector<WindowGrid>::iterator grid_iter = grid_list_.begin(); 527 grid_iter != grid_list_.end(); ++grid_iter) { 528 for (size_t i = 0; i < kSwitchableWindowContainerIdsLength; ++i) { 529 const aura::Window* container = 530 Shell::GetContainer((*grid_iter)->root_window(), 531 kSwitchableWindowContainerIds[i]); 532 for (aura::Window::Windows::const_iterator iter = 533 container->children().begin(); iter != container->children().end(); 534 ++iter) { 535 if (!(*iter)->IsVisible() || (*grid_iter)->Contains(*iter)) 536 continue; 537 hidden_windows_.Add(*iter); 538 } 539 } 540 } 541 542 // Copy the window list as it can change during iteration. 543 const aura::WindowTracker::Windows hidden_windows(hidden_windows_.windows()); 544 for (aura::WindowTracker::Windows::const_iterator iter = 545 hidden_windows.begin(); iter != hidden_windows.end(); ++iter) { 546 if (!hidden_windows_.Contains(*iter)) 547 continue; 548 ui::ScopedLayerAnimationSettings settings( 549 (*iter)->layer()->GetAnimator()); 550 settings.SetTransitionDuration(base::TimeDelta::FromMilliseconds( 551 ScopedTransformOverviewWindow::kTransitionMilliseconds)); 552 settings.SetPreemptionStrategy( 553 ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); 554 (*iter)->Hide(); 555 // Hiding the window can result in it being destroyed. 556 if (!hidden_windows_.Contains(*iter)) 557 continue; 558 (*iter)->layer()->SetOpacity(0); 559 } 560} 561 562void WindowSelector::ResetFocusRestoreWindow(bool focus) { 563 if (!restore_focus_window_) 564 return; 565 if (focus) { 566 base::AutoReset<bool> restoring_focus(&ignore_activations_, true); 567 restore_focus_window_->Focus(); 568 } 569 // If the window is in the observed_windows_ list it needs to continue to be 570 // observed. 571 if (observed_windows_.find(restore_focus_window_) == 572 observed_windows_.end()) { 573 restore_focus_window_->RemoveObserver(this); 574 } 575 restore_focus_window_ = NULL; 576} 577 578void WindowSelector::Move(Direction direction, bool animate) { 579 // Keep calling Move() on the grids until one of them reports no overflow or 580 // we made a full cycle on all the grids. 581 for (size_t i = 0; 582 i <= grid_list_.size() && 583 grid_list_[selected_grid_index_]->Move(direction, animate); i++) { 584 // TODO(flackr): If there are more than two monitors, move between grids 585 // in the requested direction. 586 selected_grid_index_ = (selected_grid_index_ + 1) % grid_list_.size(); 587 } 588} 589 590} // namespace ash 591