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/frame/caption_buttons/frame_size_button.h" 6 7 #include "ash/metrics/user_metrics_recorder.h" 8 #include "ash/screen_util.h" 9 #include "ash/shell.h" 10 #include "ash/touch/touch_uma.h" 11 #include "ash/wm/window_state.h" 12 #include "ash/wm/window_util.h" 13 #include "ash/wm/wm_event.h" 14 #include "ash/wm/workspace/phantom_window_controller.h" 15 #include "base/i18n/rtl.h" 16 #include "ui/gfx/vector2d.h" 17 #include "ui/views/widget/widget.h" 18 19 namespace { 20 21 // The default delay between the user pressing the size button and the buttons 22 // adjacent to the size button morphing into buttons for snapping left and 23 // right. 24 const int kSetButtonsToSnapModeDelayMs = 150; 25 26 // The amount that a user can overshoot one of the caption buttons while in 27 // "snap mode" and keep the button hovered/pressed. 28 const int kMaxOvershootX = 200; 29 const int kMaxOvershootY = 50; 30 31 // Returns true if a mouse drag while in "snap mode" at |location_in_screen| 32 // would hover/press |button| or keep it hovered/pressed. 33 bool HitTestButton(const ash::FrameCaptionButton* button, 34 const gfx::Point& location_in_screen) { 35 gfx::Rect expanded_bounds_in_screen = button->GetBoundsInScreen(); 36 if (button->state() == views::Button::STATE_HOVERED || 37 button->state() == views::Button::STATE_PRESSED) { 38 expanded_bounds_in_screen.Inset(-kMaxOvershootX, -kMaxOvershootY); 39 } 40 return expanded_bounds_in_screen.Contains(location_in_screen); 41 } 42 43 } // namespace 44 45 namespace ash { 46 47 FrameSizeButton::FrameSizeButton( 48 views::ButtonListener* listener, 49 views::Widget* frame, 50 FrameSizeButtonDelegate* delegate) 51 : FrameCaptionButton(listener, CAPTION_BUTTON_ICON_MAXIMIZE_RESTORE), 52 frame_(frame), 53 delegate_(delegate), 54 set_buttons_to_snap_mode_delay_ms_(kSetButtonsToSnapModeDelayMs), 55 in_snap_mode_(false), 56 snap_type_(SNAP_NONE) { 57 } 58 59 FrameSizeButton::~FrameSizeButton() { 60 } 61 62 bool FrameSizeButton::OnMousePressed(const ui::MouseEvent& event) { 63 // The minimize and close buttons are set to snap left and right when snapping 64 // is enabled. Do not enable snapping if the minimize button is not visible. 65 // The close button is always visible. 66 if (IsTriggerableEvent(event) && 67 !in_snap_mode_ && 68 delegate_->IsMinimizeButtonVisible()) { 69 StartSetButtonsToSnapModeTimer(event); 70 } 71 FrameCaptionButton::OnMousePressed(event); 72 return true; 73 } 74 75 bool FrameSizeButton::OnMouseDragged(const ui::MouseEvent& event) { 76 UpdateSnapType(event); 77 // By default a FrameCaptionButton reverts to STATE_NORMAL once the mouse 78 // leaves its bounds. Skip FrameCaptionButton's handling when 79 // |in_snap_mode_| == true because we want different behavior. 80 if (!in_snap_mode_) 81 FrameCaptionButton::OnMouseDragged(event); 82 return true; 83 } 84 85 void FrameSizeButton::OnMouseReleased(const ui::MouseEvent& event) { 86 if (!IsTriggerableEvent(event) || !CommitSnap(event)) 87 FrameCaptionButton::OnMouseReleased(event); 88 } 89 90 void FrameSizeButton::OnMouseCaptureLost() { 91 SetButtonsToNormalMode(FrameSizeButtonDelegate::ANIMATE_YES); 92 FrameCaptionButton::OnMouseCaptureLost(); 93 } 94 95 void FrameSizeButton::OnMouseMoved(const ui::MouseEvent& event) { 96 // Ignore any synthetic mouse moves during a drag. 97 if (!in_snap_mode_) 98 FrameCaptionButton::OnMouseMoved(event); 99 } 100 101 void FrameSizeButton::OnGestureEvent(ui::GestureEvent* event) { 102 if (event->details().touch_points() > 1) { 103 SetButtonsToNormalMode(FrameSizeButtonDelegate::ANIMATE_YES); 104 return; 105 } 106 107 if (event->type() == ui::ET_GESTURE_TAP_DOWN) { 108 StartSetButtonsToSnapModeTimer(*event); 109 // Go through FrameCaptionButton's handling so that the button gets pressed. 110 FrameCaptionButton::OnGestureEvent(event); 111 return; 112 } 113 114 if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN || 115 event->type() == ui::ET_GESTURE_SCROLL_UPDATE) { 116 UpdateSnapType(*event); 117 event->SetHandled(); 118 return; 119 } 120 121 if (event->type() == ui::ET_GESTURE_TAP || 122 event->type() == ui::ET_GESTURE_SCROLL_END || 123 event->type() == ui::ET_SCROLL_FLING_START || 124 event->type() == ui::ET_GESTURE_END) { 125 if (CommitSnap(*event)) { 126 if (event->type() == ui::ET_GESTURE_TAP) { 127 TouchUMA::GetInstance()->RecordGestureAction( 128 TouchUMA::GESTURE_FRAMEMAXIMIZE_TAP); 129 } 130 event->SetHandled(); 131 return; 132 } 133 } 134 135 FrameCaptionButton::OnGestureEvent(event); 136 } 137 138 void FrameSizeButton::StartSetButtonsToSnapModeTimer( 139 const ui::LocatedEvent& event) { 140 set_buttons_to_snap_mode_timer_event_location_ = event.location(); 141 if (set_buttons_to_snap_mode_delay_ms_ == 0) { 142 AnimateButtonsToSnapMode(); 143 } else { 144 set_buttons_to_snap_mode_timer_.Start( 145 FROM_HERE, 146 base::TimeDelta::FromMilliseconds(set_buttons_to_snap_mode_delay_ms_), 147 this, 148 &FrameSizeButton::AnimateButtonsToSnapMode); 149 } 150 } 151 152 void FrameSizeButton::AnimateButtonsToSnapMode() { 153 SetButtonsToSnapMode(FrameSizeButtonDelegate::ANIMATE_YES); 154 } 155 156 void FrameSizeButton::SetButtonsToSnapMode( 157 FrameSizeButtonDelegate::Animate animate) { 158 in_snap_mode_ = true; 159 160 // When using a right-to-left layout the close button is left of the size 161 // button and the minimize button is right of the size button. 162 if (base::i18n::IsRTL()) { 163 delegate_->SetButtonIcons(CAPTION_BUTTON_ICON_RIGHT_SNAPPED, 164 CAPTION_BUTTON_ICON_LEFT_SNAPPED, 165 animate); 166 } else { 167 delegate_->SetButtonIcons(CAPTION_BUTTON_ICON_LEFT_SNAPPED, 168 CAPTION_BUTTON_ICON_RIGHT_SNAPPED, 169 animate); 170 } 171 } 172 173 void FrameSizeButton::UpdateSnapType(const ui::LocatedEvent& event) { 174 if (!in_snap_mode_) { 175 // Set the buttons adjacent to the size button to snap left and right early 176 // if the user drags past the drag threshold. 177 // |set_buttons_to_snap_mode_timer_| is checked to avoid entering the snap 178 // mode as a result of an unsupported drag type (e.g. only the right mouse 179 // button is pressed). 180 gfx::Vector2d delta( 181 event.location() - set_buttons_to_snap_mode_timer_event_location_); 182 if (!set_buttons_to_snap_mode_timer_.IsRunning() || 183 !views::View::ExceededDragThreshold(delta)) { 184 return; 185 } 186 AnimateButtonsToSnapMode(); 187 } 188 189 gfx::Point event_location_in_screen(event.location()); 190 views::View::ConvertPointToScreen(this, &event_location_in_screen); 191 const FrameCaptionButton* to_hover = 192 GetButtonToHover(event_location_in_screen); 193 bool press_size_button = 194 to_hover || HitTestButton(this, event_location_in_screen); 195 196 if (to_hover) { 197 // Progress the minimize and close icon morph animations to the end if they 198 // are in progress. 199 SetButtonsToSnapMode(FrameSizeButtonDelegate::ANIMATE_NO); 200 } 201 202 delegate_->SetHoveredAndPressedButtons( 203 to_hover, press_size_button ? this : NULL); 204 205 snap_type_ = SNAP_NONE; 206 if (to_hover) { 207 switch (to_hover->icon()) { 208 case CAPTION_BUTTON_ICON_LEFT_SNAPPED: 209 snap_type_ = SNAP_LEFT; 210 break; 211 case CAPTION_BUTTON_ICON_RIGHT_SNAPPED: 212 snap_type_ = SNAP_RIGHT; 213 break; 214 case CAPTION_BUTTON_ICON_MAXIMIZE_RESTORE: 215 case CAPTION_BUTTON_ICON_MINIMIZE: 216 case CAPTION_BUTTON_ICON_CLOSE: 217 case CAPTION_BUTTON_ICON_BACK: 218 case CAPTION_BUTTON_ICON_COUNT: 219 NOTREACHED(); 220 break; 221 } 222 } 223 224 if (snap_type_ == SNAP_LEFT || snap_type_ == SNAP_RIGHT) { 225 aura::Window* window = frame_->GetNativeWindow(); 226 if (!phantom_window_controller_.get()) { 227 phantom_window_controller_.reset(new PhantomWindowController(window)); 228 } 229 gfx::Rect phantom_bounds_in_parent = (snap_type_ == SNAP_LEFT) ? 230 wm::GetDefaultLeftSnappedWindowBoundsInParent(window) : 231 wm::GetDefaultRightSnappedWindowBoundsInParent(window); 232 phantom_window_controller_->Show(ScreenUtil::ConvertRectToScreen( 233 window->parent(), phantom_bounds_in_parent)); 234 } else { 235 phantom_window_controller_.reset(); 236 } 237 } 238 239 const FrameCaptionButton* FrameSizeButton::GetButtonToHover( 240 const gfx::Point& event_location_in_screen) const { 241 const FrameCaptionButton* closest_button = delegate_->GetButtonClosestTo( 242 event_location_in_screen); 243 if ((closest_button->icon() == CAPTION_BUTTON_ICON_LEFT_SNAPPED || 244 closest_button->icon() == CAPTION_BUTTON_ICON_RIGHT_SNAPPED) && 245 HitTestButton(closest_button, event_location_in_screen)) { 246 return closest_button; 247 } 248 return NULL; 249 } 250 251 bool FrameSizeButton::CommitSnap(const ui::LocatedEvent& event) { 252 // The position of |event| may be different than the position of the previous 253 // event. 254 UpdateSnapType(event); 255 256 if (in_snap_mode_ && 257 (snap_type_ == SNAP_LEFT || snap_type_ == SNAP_RIGHT)) { 258 wm::WindowState* window_state = 259 wm::GetWindowState(frame_->GetNativeWindow()); 260 UserMetricsRecorder* metrics = Shell::GetInstance()->metrics(); 261 const wm::WMEvent snap_event( 262 snap_type_ == SNAP_LEFT ? 263 wm::WM_EVENT_SNAP_LEFT : wm::WM_EVENT_SNAP_RIGHT); 264 window_state->OnWMEvent(&snap_event); 265 metrics->RecordUserMetricsAction( 266 snap_type_ == SNAP_LEFT ? 267 UMA_WINDOW_MAXIMIZE_BUTTON_MAXIMIZE_LEFT : 268 UMA_WINDOW_MAXIMIZE_BUTTON_MAXIMIZE_RIGHT); 269 SetButtonsToNormalMode(FrameSizeButtonDelegate::ANIMATE_NO); 270 return true; 271 } 272 SetButtonsToNormalMode(FrameSizeButtonDelegate::ANIMATE_YES); 273 return false; 274 } 275 276 void FrameSizeButton::SetButtonsToNormalMode( 277 FrameSizeButtonDelegate::Animate animate) { 278 in_snap_mode_ = false; 279 snap_type_ = SNAP_NONE; 280 set_buttons_to_snap_mode_timer_.Stop(); 281 delegate_->SetButtonsToNormal(animate); 282 phantom_window_controller_.reset(); 283 } 284 285 } // namespace ash 286