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
19namespace {
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.
24const 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.
28const int kMaxOvershootX = 200;
29const 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.
33bool 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
45namespace ash {
46
47FrameSizeButton::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
59FrameSizeButton::~FrameSizeButton() {
60}
61
62bool 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
75bool 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
85void FrameSizeButton::OnMouseReleased(const ui::MouseEvent& event) {
86  if (!IsTriggerableEvent(event) || !CommitSnap(event))
87    FrameCaptionButton::OnMouseReleased(event);
88}
89
90void FrameSizeButton::OnMouseCaptureLost() {
91  SetButtonsToNormalMode(FrameSizeButtonDelegate::ANIMATE_YES);
92  FrameCaptionButton::OnMouseCaptureLost();
93}
94
95void 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
101void 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
138void 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
152void FrameSizeButton::AnimateButtonsToSnapMode() {
153  SetButtonsToSnapMode(FrameSizeButtonDelegate::ANIMATE_YES);
154}
155
156void 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
173void 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_COUNT:
218        NOTREACHED();
219        break;
220    }
221  }
222
223  if (snap_type_ == SNAP_LEFT || snap_type_ == SNAP_RIGHT) {
224    aura::Window* window = frame_->GetNativeWindow();
225    if (!phantom_window_controller_.get()) {
226      phantom_window_controller_.reset(new PhantomWindowController(window));
227    }
228    gfx::Rect phantom_bounds_in_parent = (snap_type_ == SNAP_LEFT) ?
229        wm::GetDefaultLeftSnappedWindowBoundsInParent(window) :
230        wm::GetDefaultRightSnappedWindowBoundsInParent(window);
231    phantom_window_controller_->Show(ScreenUtil::ConvertRectToScreen(
232          window->parent(), phantom_bounds_in_parent));
233  } else {
234    phantom_window_controller_.reset();
235  }
236}
237
238const FrameCaptionButton* FrameSizeButton::GetButtonToHover(
239    const gfx::Point& event_location_in_screen) const {
240  const FrameCaptionButton* closest_button = delegate_->GetButtonClosestTo(
241      event_location_in_screen);
242  if ((closest_button->icon() == CAPTION_BUTTON_ICON_LEFT_SNAPPED ||
243       closest_button->icon() == CAPTION_BUTTON_ICON_RIGHT_SNAPPED) &&
244      HitTestButton(closest_button, event_location_in_screen)) {
245    return closest_button;
246  }
247  return NULL;
248}
249
250bool FrameSizeButton::CommitSnap(const ui::LocatedEvent& event) {
251  // The position of |event| may be different than the position of the previous
252  // event.
253  UpdateSnapType(event);
254
255  if (in_snap_mode_ &&
256      (snap_type_ == SNAP_LEFT || snap_type_ == SNAP_RIGHT)) {
257    wm::WindowState* window_state =
258        wm::GetWindowState(frame_->GetNativeWindow());
259    UserMetricsRecorder* metrics = Shell::GetInstance()->metrics();
260    const wm::WMEvent snap_event(
261        snap_type_ == SNAP_LEFT ?
262        wm::WM_EVENT_SNAP_LEFT : wm::WM_EVENT_SNAP_RIGHT);
263    window_state->OnWMEvent(&snap_event);
264    metrics->RecordUserMetricsAction(
265        snap_type_ == SNAP_LEFT ?
266        UMA_WINDOW_MAXIMIZE_BUTTON_MAXIMIZE_LEFT :
267        UMA_WINDOW_MAXIMIZE_BUTTON_MAXIMIZE_RIGHT);
268    SetButtonsToNormalMode(FrameSizeButtonDelegate::ANIMATE_NO);
269    return true;
270  }
271  SetButtonsToNormalMode(FrameSizeButtonDelegate::ANIMATE_YES);
272  return false;
273}
274
275void FrameSizeButton::SetButtonsToNormalMode(
276    FrameSizeButtonDelegate::Animate animate) {
277  in_snap_mode_ = false;
278  snap_type_ = SNAP_NONE;
279  set_buttons_to_snap_mode_timer_.Stop();
280  delegate_->SetButtonsToNormal(animate);
281  phantom_window_controller_.reset();
282}
283
284}  // namespace ash
285