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/caption_buttons/frame_maximize_button.h" 6 7 #include "ash/metrics/user_metrics_recorder.h" 8 #include "ash/screen_ash.h" 9 #include "ash/shelf/shelf_widget.h" 10 #include "ash/shell.h" 11 #include "ash/touch/touch_uma.h" 12 #include "ash/wm/caption_buttons/frame_maximize_button_observer.h" 13 #include "ash/wm/caption_buttons/maximize_bubble_controller.h" 14 #include "ash/wm/window_animations.h" 15 #include "ash/wm/window_state.h" 16 #include "ash/wm/workspace/phantom_window_controller.h" 17 #include "ash/wm/workspace/snap_sizer.h" 18 #include "grit/ash_strings.h" 19 #include "ui/aura/window.h" 20 #include "ui/base/l10n/l10n_util.h" 21 #include "ui/base/resource/resource_bundle.h" 22 #include "ui/events/event.h" 23 #include "ui/events/event_handler.h" 24 #include "ui/gfx/image/image.h" 25 #include "ui/gfx/screen.h" 26 #include "ui/views/widget/widget.h" 27 #include "ui/views/window/non_client_view.h" 28 29 using ash::internal::SnapSizer; 30 31 namespace ash { 32 33 namespace { 34 35 // Delay before forcing an update of the snap location. 36 const int kUpdateDelayMS = 400; 37 38 // The delay of the bubble appearance. 39 const int kBubbleAppearanceDelayMS = 500; 40 41 // The minimum sanp size in percent of the screen width. 42 const int kMinSnapSizePercent = 50; 43 } 44 45 // EscapeEventFilter is installed on the RootWindow to track when the escape key 46 // is pressed. We use an EventFilter for this as the FrameMaximizeButton 47 // normally does not get focus. 48 class FrameMaximizeButton::EscapeEventFilter : public ui::EventHandler { 49 public: 50 explicit EscapeEventFilter(FrameMaximizeButton* button); 51 virtual ~EscapeEventFilter(); 52 53 // EventFilter overrides: 54 virtual void OnKeyEvent(ui::KeyEvent* event) OVERRIDE; 55 56 private: 57 FrameMaximizeButton* button_; 58 59 DISALLOW_COPY_AND_ASSIGN(EscapeEventFilter); 60 }; 61 62 FrameMaximizeButton::EscapeEventFilter::EscapeEventFilter( 63 FrameMaximizeButton* button) 64 : button_(button) { 65 Shell::GetInstance()->AddPreTargetHandler(this); 66 } 67 68 FrameMaximizeButton::EscapeEventFilter::~EscapeEventFilter() { 69 Shell::GetInstance()->RemovePreTargetHandler(this); 70 } 71 72 void FrameMaximizeButton::EscapeEventFilter::OnKeyEvent( 73 ui::KeyEvent* event) { 74 if (event->type() == ui::ET_KEY_PRESSED && 75 event->key_code() == ui::VKEY_ESCAPE) { 76 button_->Cancel(false); 77 } 78 } 79 80 // FrameMaximizeButton --------------------------------------------------------- 81 82 FrameMaximizeButton::FrameMaximizeButton(views::ButtonListener* listener, 83 views::Widget* frame) 84 : FrameCaptionButton(listener, CAPTION_BUTTON_ICON_MAXIMIZE_RESTORE), 85 frame_(frame), 86 observing_frame_(false), 87 is_snap_enabled_(false), 88 exceeded_drag_threshold_(false), 89 press_is_gesture_(false), 90 snap_type_(SNAP_NONE), 91 bubble_appearance_delay_ms_(kBubbleAppearanceDelayMS) { 92 // TODO(sky): nuke this. It's temporary while we don't have good images. 93 SetImageAlignment(ALIGN_LEFT, ALIGN_BOTTOM); 94 } 95 96 FrameMaximizeButton::~FrameMaximizeButton() { 97 // Before the window gets destroyed, the maximizer dialog needs to be shut 98 // down since it would otherwise call into a deleted object. 99 maximizer_.reset(); 100 if (observing_frame_) 101 OnWindowDestroying(frame_->GetNativeWindow()); 102 } 103 104 void FrameMaximizeButton::AddObserver(FrameMaximizeButtonObserver* observer) { 105 observer_list_.AddObserver(observer); 106 } 107 108 void FrameMaximizeButton::RemoveObserver( 109 FrameMaximizeButtonObserver* observer) { 110 observer_list_.RemoveObserver(observer); 111 } 112 113 void FrameMaximizeButton::SnapButtonHovered(SnapType type) { 114 // Make sure to only show hover operations when no button is pressed and 115 // a similar snap operation in progress does not get re-applied. 116 if (is_snap_enabled_ || (type == snap_type_ && snap_sizer_)) 117 return; 118 // Prime the mouse location with the center of the (local) button. 119 press_location_ = gfx::Point(width() / 2, height() / 2); 120 // Then get an adjusted mouse position to initiate the effect. 121 gfx::Point location = press_location_; 122 switch (type) { 123 case SNAP_LEFT: 124 location.set_x(location.x() - width()); 125 break; 126 case SNAP_RIGHT: 127 location.set_x(location.x() + width()); 128 break; 129 case SNAP_MINIMIZE: 130 location.set_y(location.y() + height()); 131 break; 132 case SNAP_RESTORE: 133 // Simulate a mouse button move over the according button. 134 if (GetMaximizeBubbleFrameState() == FRAME_STATE_SNAP_LEFT) 135 location.set_x(location.x() - width()); 136 else if (GetMaximizeBubbleFrameState() == FRAME_STATE_SNAP_RIGHT) 137 location.set_x(location.x() + width()); 138 break; 139 case SNAP_MAXIMIZE: 140 break; 141 case SNAP_NONE: 142 Cancel(true); 143 return; 144 default: 145 // We should not come here. 146 NOTREACHED(); 147 } 148 // Note: There is no hover with touch - we can therefore pass false for touch 149 // operations. 150 UpdateSnap(location, true, false); 151 } 152 153 void FrameMaximizeButton::ExecuteSnapAndCloseMenu(SnapType snap_type) { 154 // We can come here with no snap type set in case that the mouse opened the 155 // maximize button and a touch event "touched" a button. 156 if (snap_type_ == SNAP_NONE) 157 SnapButtonHovered(snap_type); 158 159 Cancel(true); 160 // Tell our menu to close. 161 maximizer_.reset(); 162 snap_type_ = snap_type; 163 // Since Snap might destroy |this|, but the snap_sizer needs to be destroyed, 164 // The ownership of the snap_sizer is taken now. 165 scoped_ptr<SnapSizer> snap_sizer(snap_sizer_.release()); 166 Snap(snap_sizer.get()); 167 } 168 169 void FrameMaximizeButton::OnMaximizeBubbleShown(views::Widget* bubble) { 170 FOR_EACH_OBSERVER(FrameMaximizeButtonObserver, 171 observer_list_, 172 OnMaximizeBubbleShown(bubble)); 173 } 174 175 void FrameMaximizeButton::DestroyMaximizeMenu() { 176 Cancel(false); 177 } 178 179 void FrameMaximizeButton::OnWindowBoundsChanged( 180 aura::Window* window, 181 const gfx::Rect& old_bounds, 182 const gfx::Rect& new_bounds) { 183 Cancel(false); 184 } 185 186 void FrameMaximizeButton::OnWindowPropertyChanged(aura::Window* window, 187 const void* key, 188 intptr_t old) { 189 Cancel(false); 190 } 191 192 void FrameMaximizeButton::OnWindowDestroying(aura::Window* window) { 193 maximizer_.reset(); 194 if (observing_frame_) { 195 CHECK_EQ(frame_->GetNativeWindow(), window); 196 frame_->GetNativeWindow()->RemoveObserver(this); 197 frame_->RemoveObserver(this); 198 observing_frame_ = false; 199 } 200 } 201 202 void FrameMaximizeButton::OnWidgetActivationChanged(views::Widget* widget, 203 bool active) { 204 // Upon losing focus, the bubble menu and the phantom window should hide. 205 if (!active) 206 Cancel(false); 207 } 208 209 bool FrameMaximizeButton::OnMousePressed(const ui::MouseEvent& event) { 210 // If we are already in a mouse click / drag operation, a second button down 211 // call will cancel (this addresses crbug.com/143755). 212 if (is_snap_enabled_) { 213 Cancel(false); 214 } else { 215 is_snap_enabled_ = event.IsOnlyLeftMouseButton(); 216 if (is_snap_enabled_) 217 ProcessStartEvent(event); 218 } 219 ImageButton::OnMousePressed(event); 220 return true; 221 } 222 223 void FrameMaximizeButton::OnMouseEntered(const ui::MouseEvent& event) { 224 ImageButton::OnMouseEntered(event); 225 if (!maximizer_) { 226 DCHECK(GetWidget()); 227 if (!observing_frame_) { 228 observing_frame_ = true; 229 frame_->GetNativeWindow()->AddObserver(this); 230 frame_->AddObserver(this); 231 } 232 maximizer_.reset(new MaximizeBubbleController( 233 this, 234 GetMaximizeBubbleFrameState(), 235 bubble_appearance_delay_ms_)); 236 } 237 } 238 239 void FrameMaximizeButton::OnMouseExited(const ui::MouseEvent& event) { 240 ImageButton::OnMouseExited(event); 241 // Remove the bubble menu when the button is not pressed and the mouse is not 242 // within the bubble. 243 if (!is_snap_enabled_ && maximizer_) { 244 if (maximizer_->GetBubbleWindow()) { 245 gfx::Point screen_location = Shell::GetScreen()->GetCursorScreenPoint(); 246 if (!maximizer_->GetBubbleWindow()->GetBoundsInScreen().Contains( 247 screen_location)) { 248 maximizer_.reset(); 249 // Make sure that all remaining snap hover states get removed. 250 SnapButtonHovered(SNAP_NONE); 251 } 252 } else { 253 // The maximize dialog does not show up immediately after creating the 254 // |maximizer_|. Destroy the dialog therefore before it shows up. 255 maximizer_.reset(); 256 } 257 } 258 } 259 260 bool FrameMaximizeButton::OnMouseDragged(const ui::MouseEvent& event) { 261 if (is_snap_enabled_) 262 ProcessUpdateEvent(event); 263 return ImageButton::OnMouseDragged(event); 264 } 265 266 void FrameMaximizeButton::OnMouseReleased(const ui::MouseEvent& event) { 267 maximizer_.reset(); 268 bool snap_was_enabled = is_snap_enabled_; 269 if (!ProcessEndEvent(event) && snap_was_enabled) 270 ImageButton::OnMouseReleased(event); 271 // At this point |this| might be already destroyed. 272 } 273 274 void FrameMaximizeButton::OnMouseCaptureLost() { 275 Cancel(false); 276 ImageButton::OnMouseCaptureLost(); 277 } 278 279 void FrameMaximizeButton::OnGestureEvent(ui::GestureEvent* event) { 280 if (event->type() == ui::ET_GESTURE_TAP_DOWN) { 281 is_snap_enabled_ = true; 282 ProcessStartEvent(*event); 283 event->SetHandled(); 284 return; 285 } 286 287 if (event->type() == ui::ET_GESTURE_TAP || 288 (event->type() == ui::ET_GESTURE_SCROLL_END && is_snap_enabled_) || 289 event->type() == ui::ET_SCROLL_FLING_START) { 290 // The position of the event may have changed from the previous event (both 291 // for TAP and SCROLL_END). So it is necessary to update the snap-state for 292 // the current event. 293 ProcessUpdateEvent(*event); 294 if (event->type() == ui::ET_GESTURE_TAP) { 295 snap_type_ = SnapTypeForLocation(event->location()); 296 TouchUMA::GetInstance()->RecordGestureAction( 297 TouchUMA::GESTURE_FRAMEMAXIMIZE_TAP); 298 } 299 ProcessEndEvent(*event); 300 event->SetHandled(); 301 return; 302 } 303 304 if (is_snap_enabled_) { 305 if (event->type() == ui::ET_GESTURE_END && 306 event->details().touch_points() == 1) { 307 // The position of the event may have changed from the previous event. So 308 // it is necessary to update the snap-state for the current event. 309 ProcessUpdateEvent(*event); 310 snap_type_ = SnapTypeForLocation(event->location()); 311 ProcessEndEvent(*event); 312 event->SetHandled(); 313 return; 314 } 315 316 if (event->type() == ui::ET_GESTURE_SCROLL_UPDATE || 317 event->type() == ui::ET_GESTURE_SCROLL_BEGIN) { 318 ProcessUpdateEvent(*event); 319 event->SetHandled(); 320 return; 321 } 322 } 323 324 ImageButton::OnGestureEvent(event); 325 } 326 327 void FrameMaximizeButton::SetVisible(bool visible) { 328 views::View::SetVisible(visible); 329 } 330 331 void FrameMaximizeButton::ProcessStartEvent(const ui::LocatedEvent& event) { 332 DCHECK(is_snap_enabled_); 333 // Prepare the help menu. 334 if (!maximizer_) { 335 maximizer_.reset(new MaximizeBubbleController( 336 this, 337 GetMaximizeBubbleFrameState(), 338 bubble_appearance_delay_ms_)); 339 } else { 340 // If the menu did not show up yet, we delay it even a bit more. 341 maximizer_->DelayCreation(); 342 } 343 snap_sizer_.reset(NULL); 344 InstallEventFilter(); 345 snap_type_ = SNAP_NONE; 346 press_location_ = event.location(); 347 press_is_gesture_ = event.IsGestureEvent(); 348 exceeded_drag_threshold_ = false; 349 update_timer_.Start( 350 FROM_HERE, 351 base::TimeDelta::FromMilliseconds(kUpdateDelayMS), 352 this, 353 &FrameMaximizeButton::UpdateSnapFromEventLocation); 354 } 355 356 void FrameMaximizeButton::ProcessUpdateEvent(const ui::LocatedEvent& event) { 357 DCHECK(is_snap_enabled_); 358 if (!exceeded_drag_threshold_) { 359 exceeded_drag_threshold_ = views::View::ExceededDragThreshold( 360 event.location() - press_location_); 361 } 362 if (exceeded_drag_threshold_) 363 UpdateSnap(event.location(), false, event.IsGestureEvent()); 364 } 365 366 bool FrameMaximizeButton::ProcessEndEvent(const ui::LocatedEvent& event) { 367 update_timer_.Stop(); 368 UninstallEventFilter(); 369 bool should_snap = is_snap_enabled_; 370 is_snap_enabled_ = false; 371 372 // Remove our help bubble. 373 maximizer_.reset(); 374 375 if (!should_snap || snap_type_ == SNAP_NONE) 376 return false; 377 378 SetState(views::CustomButton::STATE_NORMAL); 379 // SetState will not call SchedulePaint() if state was already set to 380 // STATE_NORMAL during a drag. 381 SchedulePaint(); 382 phantom_window_.reset(); 383 // Since Snap might destroy |this|, but the snap_sizer needs to be destroyed, 384 // The ownership of the snap_sizer is taken now. 385 scoped_ptr<SnapSizer> snap_sizer(snap_sizer_.release()); 386 Snap(snap_sizer.get()); 387 return true; 388 } 389 390 void FrameMaximizeButton::Cancel(bool keep_menu_open) { 391 if (!keep_menu_open) { 392 maximizer_.reset(); 393 UninstallEventFilter(); 394 is_snap_enabled_ = false; 395 snap_sizer_.reset(); 396 } 397 phantom_window_.reset(); 398 snap_type_ = SNAP_NONE; 399 update_timer_.Stop(); 400 SchedulePaint(); 401 } 402 403 void FrameMaximizeButton::InstallEventFilter() { 404 if (escape_event_filter_) 405 return; 406 407 escape_event_filter_.reset(new EscapeEventFilter(this)); 408 } 409 410 void FrameMaximizeButton::UninstallEventFilter() { 411 escape_event_filter_.reset(NULL); 412 } 413 414 void FrameMaximizeButton::UpdateSnapFromEventLocation() { 415 // If the drag threshold has been exceeded the snap location is up to date. 416 if (exceeded_drag_threshold_) 417 return; 418 exceeded_drag_threshold_ = true; 419 UpdateSnap(press_location_, false, press_is_gesture_); 420 } 421 422 void FrameMaximizeButton::UpdateSnap(const gfx::Point& location, 423 bool select_default, 424 bool is_touch) { 425 SnapType type = SnapTypeForLocation(location); 426 if (type == snap_type_) { 427 if (snap_sizer_) { 428 snap_sizer_->Update(LocationForSnapSizer(location)); 429 phantom_window_->Show(ScreenAsh::ConvertRectToScreen( 430 frame_->GetNativeView()->parent(), 431 snap_sizer_->target_bounds())); 432 } 433 return; 434 } 435 436 snap_type_ = type; 437 snap_sizer_.reset(); 438 SchedulePaint(); 439 440 if (snap_type_ == SNAP_NONE) { 441 phantom_window_.reset(); 442 return; 443 } 444 445 if (snap_type_ == SNAP_LEFT || snap_type_ == SNAP_RIGHT) { 446 SnapSizer::Edge snap_edge = snap_type_ == SNAP_LEFT ? 447 SnapSizer::LEFT_EDGE : SnapSizer::RIGHT_EDGE; 448 SnapSizer::InputType input_type = 449 is_touch ? SnapSizer::TOUCH_MAXIMIZE_BUTTON_INPUT : 450 SnapSizer::OTHER_INPUT; 451 snap_sizer_.reset(new SnapSizer( 452 wm::GetWindowState(frame_->GetNativeWindow()), 453 LocationForSnapSizer(location), 454 snap_edge, 455 input_type)); 456 if (select_default) 457 snap_sizer_->SelectDefaultSizeAndDisableResize(); 458 } 459 if (!phantom_window_) { 460 phantom_window_.reset(new internal::PhantomWindowController( 461 frame_->GetNativeWindow())); 462 } 463 if (maximizer_) { 464 phantom_window_->set_phantom_below_window(maximizer_->GetBubbleWindow()); 465 maximizer_->SetSnapType(snap_type_); 466 } 467 phantom_window_->Show( 468 ScreenBoundsForType(snap_type_, *snap_sizer_.get())); 469 } 470 471 SnapType FrameMaximizeButton::SnapTypeForLocation( 472 const gfx::Point& location) const { 473 MaximizeBubbleFrameState maximize_type = GetMaximizeBubbleFrameState(); 474 gfx::Vector2d delta(location - press_location_); 475 if (!views::View::ExceededDragThreshold(delta)) 476 return maximize_type != FRAME_STATE_FULL ? SNAP_MAXIMIZE : SNAP_RESTORE; 477 if (delta.x() < 0 && delta.y() > delta.x() && delta.y() < -delta.x()) 478 return maximize_type == FRAME_STATE_SNAP_LEFT ? SNAP_RESTORE : SNAP_LEFT; 479 if (delta.x() > 0 && delta.y() > -delta.x() && delta.y() < delta.x()) 480 return maximize_type == FRAME_STATE_SNAP_RIGHT ? SNAP_RESTORE : SNAP_RIGHT; 481 if (delta.y() > 0) 482 return SNAP_MINIMIZE; 483 return maximize_type != FRAME_STATE_FULL ? SNAP_MAXIMIZE : SNAP_RESTORE; 484 } 485 486 gfx::Rect FrameMaximizeButton::ScreenBoundsForType( 487 SnapType type, 488 const SnapSizer& snap_sizer) const { 489 aura::Window* window = frame_->GetNativeWindow(); 490 switch (type) { 491 case SNAP_LEFT: 492 case SNAP_RIGHT: 493 return ScreenAsh::ConvertRectToScreen(window->parent(), 494 snap_sizer.target_bounds()); 495 case SNAP_MAXIMIZE: 496 return ScreenAsh::ConvertRectToScreen( 497 window->parent(), 498 ScreenAsh::GetMaximizedWindowBoundsInParent(window)); 499 case SNAP_MINIMIZE: { 500 gfx::Rect rect = GetMinimizeAnimationTargetBoundsInScreen(window); 501 if (!rect.IsEmpty()) { 502 // PhantomWindowController insets slightly, outset it so the phantom 503 // doesn't appear inset. 504 rect.Inset(-8, -8); 505 } 506 return rect; 507 } 508 case SNAP_RESTORE: { 509 wm::WindowState* window_state = wm::GetWindowState(window); 510 return window_state->HasRestoreBounds() ? 511 window_state->GetRestoreBoundsInScreen() : 512 frame_->GetWindowBoundsInScreen(); 513 } 514 case SNAP_NONE: 515 NOTREACHED(); 516 } 517 return gfx::Rect(); 518 } 519 520 gfx::Point FrameMaximizeButton::LocationForSnapSizer( 521 const gfx::Point& location) const { 522 gfx::Point result(location); 523 views::View::ConvertPointToScreen(this, &result); 524 return result; 525 } 526 527 void FrameMaximizeButton::Snap(SnapSizer* snap_sizer) { 528 Shell* shell = Shell::GetInstance(); 529 switch (snap_type_) { 530 case SNAP_LEFT: 531 case SNAP_RIGHT: { 532 snap_sizer->SnapWindowToTargetBounds(); 533 shell->metrics()->RecordUserMetricsAction( 534 snap_type_ == SNAP_LEFT ? 535 UMA_WINDOW_MAXIMIZE_BUTTON_MAXIMIZE_LEFT : 536 UMA_WINDOW_MAXIMIZE_BUTTON_MAXIMIZE_RIGHT); 537 break; 538 } 539 case SNAP_MAXIMIZE: 540 frame_->Maximize(); 541 shell->metrics()->RecordUserMetricsAction( 542 UMA_WINDOW_MAXIMIZE_BUTTON_MAXIMIZE); 543 break; 544 case SNAP_MINIMIZE: 545 frame_->Minimize(); 546 shell->metrics()->RecordUserMetricsAction( 547 UMA_WINDOW_MAXIMIZE_BUTTON_MINIMIZE); 548 break; 549 case SNAP_RESTORE: 550 frame_->Restore(); 551 shell->metrics()->RecordUserMetricsAction( 552 UMA_WINDOW_MAXIMIZE_BUTTON_RESTORE); 553 break; 554 case SNAP_NONE: 555 NOTREACHED(); 556 } 557 } 558 559 MaximizeBubbleFrameState 560 FrameMaximizeButton::GetMaximizeBubbleFrameState() const { 561 wm::WindowState* window_state = 562 wm::GetWindowState(frame_->GetNativeWindow()); 563 // When there are no restore bounds, we are in normal mode. 564 if (!window_state->HasRestoreBounds()) 565 return FRAME_STATE_NONE; 566 // The normal maximized test can be used. 567 if (frame_->IsMaximized()) 568 return FRAME_STATE_FULL; 569 // For Left/right maximize we need to check the dimensions. 570 gfx::Rect bounds = frame_->GetWindowBoundsInScreen(); 571 gfx::Rect screen = Shell::GetScreen()->GetDisplayNearestWindow( 572 frame_->GetNativeView()).work_area(); 573 if (bounds.width() < (screen.width() * kMinSnapSizePercent) / 100) 574 return FRAME_STATE_NONE; 575 // We might still have a horizontally filled window at this point which we 576 // treat as no special state. 577 if (bounds.y() != screen.y() || bounds.height() != screen.height()) 578 return FRAME_STATE_NONE; 579 580 // We have to be in a maximize mode at this point. 581 if (bounds.x() == screen.x()) 582 return FRAME_STATE_SNAP_LEFT; 583 if (bounds.right() == screen.right()) 584 return FRAME_STATE_SNAP_RIGHT; 585 // If we come here, it is likely caused by the fact that the 586 // "VerticalResizeDoubleClick" stored a restore rectangle. In that case 587 // we allow all maximize operations (and keep the restore rectangle). 588 return FRAME_STATE_NONE; 589 } 590 591 } // namespace ash 592