1 // Copyright (c) 2012 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.
5 #include "ash/wm/workspace/frame_maximize_button.h"
7 #include "ash/launcher/launcher.h"
8 #include "ash/screen_ash.h"
9 #include "ash/shelf/shelf_widget.h"
10 #include "ash/shell.h"
11 #include "ash/shell_delegate.h"
12 #include "ash/wm/maximize_bubble_controller.h"
13 #include "ash/wm/property_util.h"
14 #include "ash/wm/window_properties.h"
15 #include "ash/wm/window_util.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/events/event.h"
21 #include "ui/base/events/event_handler.h"
22 #include "ui/base/l10n/l10n_util.h"
23 #include "ui/base/resource/resource_bundle.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"
29 using ash::internal::SnapSizer
;
35 // Delay before forcing an update of the snap location.
36 const int kUpdateDelayMS
= 400;
38 // The delay of the bubble appearance.
39 const int kBubbleAppearanceDelayMS
= 500;
41 // The minimum sanp size in percent of the screen width.
42 const int kMinSnapSizePercent
= 50;
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
{
50 explicit EscapeEventFilter(FrameMaximizeButton
* button
);
51 virtual ~EscapeEventFilter();
53 // EventFilter overrides:
54 virtual void OnKeyEvent(ui::KeyEvent
* event
) OVERRIDE
;
57 FrameMaximizeButton
* button_
;
59 DISALLOW_COPY_AND_ASSIGN(EscapeEventFilter
);
62 FrameMaximizeButton::EscapeEventFilter::EscapeEventFilter(
63 FrameMaximizeButton
* button
)
65 Shell::GetInstance()->AddPreTargetHandler(this);
68 FrameMaximizeButton::EscapeEventFilter::~EscapeEventFilter() {
69 Shell::GetInstance()->RemovePreTargetHandler(this);
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);
80 // FrameMaximizeButton ---------------------------------------------------------
82 FrameMaximizeButton::FrameMaximizeButton(views::ButtonListener
* listener
,
83 views::NonClientFrameView
* frame
)
84 : ImageButton(listener
),
86 is_snap_enabled_(false),
87 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
);
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.
101 OnWindowDestroying(widget_
->GetNativeWindow());
104 void FrameMaximizeButton::SnapButtonHovered(SnapType type
) {
105 // Make sure to only show hover operations when no button is pressed and
106 // a similar snap operation in progress does not get re-applied.
107 if (is_snap_enabled_
|| (type
== snap_type_
&& snap_sizer_
.get()))
109 // Prime the mouse location with the center of the (local) button.
110 press_location_
= gfx::Point(width() / 2, height() / 2);
111 // Then get an adjusted mouse position to initiate the effect.
112 gfx::Point location
= press_location_
;
115 location
.set_x(location
.x() - width());
118 location
.set_x(location
.x() + width());
121 location
.set_y(location
.y() + height());
124 // Simulate a mouse button move over the according button.
125 if (GetMaximizeBubbleFrameState() == FRAME_STATE_SNAP_LEFT
)
126 location
.set_x(location
.x() - width());
127 else if (GetMaximizeBubbleFrameState() == FRAME_STATE_SNAP_RIGHT
)
128 location
.set_x(location
.x() + width());
136 // We should not come here.
139 // Note: There is no hover with touch - we can therefore pass false for touch
141 UpdateSnap(location
, true, false);
144 void FrameMaximizeButton::ExecuteSnapAndCloseMenu(SnapType snap_type
) {
145 DCHECK_NE(snap_type_
, SNAP_NONE
);
147 // Tell our menu to close.
149 snap_type_
= snap_type
;
150 // Since Snap might destroy |this|, but the snap_sizer needs to be destroyed,
151 // The ownership of the snap_sizer is taken now.
152 scoped_ptr
<SnapSizer
> snap_sizer(snap_sizer_
.release());
153 Snap(*snap_sizer
.get());
156 void FrameMaximizeButton::DestroyMaximizeMenu() {
160 void FrameMaximizeButton::OnWindowBoundsChanged(
161 aura::Window
* window
,
162 const gfx::Rect
& old_bounds
,
163 const gfx::Rect
& new_bounds
) {
167 void FrameMaximizeButton::OnWindowPropertyChanged(aura::Window
* window
,
170 // Changing the window position is managed status should not Cancel.
171 // Note that this case might happen when a non user managed window
172 // transitions from maximized to L/R maximized.
173 if (key
!= ash::internal::kWindowPositionManagedKey
)
177 void FrameMaximizeButton::OnWindowDestroying(aura::Window
* window
) {
180 CHECK_EQ(widget_
->GetNativeWindow(), window
);
181 widget_
->GetNativeWindow()->RemoveObserver(this);
182 widget_
->RemoveObserver(this);
187 void FrameMaximizeButton::OnWidgetActivationChanged(views::Widget
* widget
,
189 // Upon losing focus, the control bubble should hide.
190 if (!active
&& maximizer_
.get())
194 bool FrameMaximizeButton::OnMousePressed(const ui::MouseEvent
& event
) {
195 // If we are already in a mouse click / drag operation, a second button down
196 // call will cancel (this addresses crbug.com/143755).
197 if (is_snap_enabled_
) {
200 is_snap_enabled_
= event
.IsOnlyLeftMouseButton();
201 if (is_snap_enabled_
)
202 ProcessStartEvent(event
);
204 ImageButton::OnMousePressed(event
);
208 void FrameMaximizeButton::OnMouseEntered(const ui::MouseEvent
& event
) {
209 ImageButton::OnMouseEntered(event
);
210 if (!maximizer_
.get()) {
213 widget_
= frame_
->GetWidget();
214 widget_
->GetNativeWindow()->AddObserver(this);
215 widget_
->AddObserver(this);
217 maximizer_
.reset(new MaximizeBubbleController(
219 GetMaximizeBubbleFrameState(),
220 bubble_appearance_delay_ms_
));
224 void FrameMaximizeButton::OnMouseExited(const ui::MouseEvent
& event
) {
225 ImageButton::OnMouseExited(event
);
226 // Remove the bubble menu when the button is not pressed and the mouse is not
227 // within the bubble.
228 if (!is_snap_enabled_
&& maximizer_
.get()) {
229 if (maximizer_
->GetBubbleWindow()) {
230 gfx::Point screen_location
= Shell::GetScreen()->GetCursorScreenPoint();
231 if (!maximizer_
->GetBubbleWindow()->GetBoundsInScreen().Contains(
234 // Make sure that all remaining snap hover states get removed.
235 SnapButtonHovered(SNAP_NONE
);
238 // The maximize dialog does not show up immediately after creating the
239 // |mazimizer_|. Destroy the dialog therefore before it shows up.
245 bool FrameMaximizeButton::OnMouseDragged(const ui::MouseEvent
& event
) {
246 if (is_snap_enabled_
)
247 ProcessUpdateEvent(event
);
248 return ImageButton::OnMouseDragged(event
);
251 void FrameMaximizeButton::OnMouseReleased(const ui::MouseEvent
& event
) {
253 bool snap_was_enabled
= is_snap_enabled_
;
254 if (!ProcessEndEvent(event
) && snap_was_enabled
)
255 ImageButton::OnMouseReleased(event
);
256 // At this point |this| might be already destroyed.
259 void FrameMaximizeButton::OnMouseCaptureLost() {
261 ImageButton::OnMouseCaptureLost();
264 void FrameMaximizeButton::OnGestureEvent(ui::GestureEvent
* event
) {
265 if (event
->type() == ui::ET_GESTURE_TAP_DOWN
) {
266 is_snap_enabled_
= true;
267 ProcessStartEvent(*event
);
272 if (event
->type() == ui::ET_GESTURE_TAP
||
273 event
->type() == ui::ET_GESTURE_SCROLL_END
||
274 event
->type() == ui::ET_SCROLL_FLING_START
) {
275 // The position of the event may have changed from the previous event (both
276 // for TAP and SCROLL_END). So it is necessary to update the snap-state for
277 // the current event.
278 ProcessUpdateEvent(*event
);
279 if (event
->type() == ui::ET_GESTURE_TAP
)
280 snap_type_
= SnapTypeForLocation(event
->location());
281 ProcessEndEvent(*event
);
286 if (is_snap_enabled_
) {
287 if (event
->type() == ui::ET_GESTURE_END
&&
288 event
->details().touch_points() == 1) {
289 // The position of the event may have changed from the previous event. So
290 // it is necessary to update the snap-state for the current event.
291 ProcessUpdateEvent(*event
);
292 snap_type_
= SnapTypeForLocation(event
->location());
293 ProcessEndEvent(*event
);
298 if (event
->type() == ui::ET_GESTURE_SCROLL_UPDATE
||
299 event
->type() == ui::ET_GESTURE_SCROLL_BEGIN
) {
300 ProcessUpdateEvent(*event
);
306 ImageButton::OnGestureEvent(event
);
309 void FrameMaximizeButton::ProcessStartEvent(const ui::LocatedEvent
& event
) {
310 DCHECK(is_snap_enabled_
);
311 // Prepare the help menu.
312 if (!maximizer_
.get()) {
313 maximizer_
.reset(new MaximizeBubbleController(
315 GetMaximizeBubbleFrameState(),
316 bubble_appearance_delay_ms_
));
318 // If the menu did not show up yet, we delay it even a bit more.
319 maximizer_
->DelayCreation();
321 snap_sizer_
.reset(NULL
);
322 InstallEventFilter();
323 snap_type_
= SNAP_NONE
;
324 press_location_
= event
.location();
325 press_is_gesture_
= event
.IsGestureEvent();
326 exceeded_drag_threshold_
= false;
329 base::TimeDelta::FromMilliseconds(kUpdateDelayMS
),
331 &FrameMaximizeButton::UpdateSnapFromEventLocation
);
334 void FrameMaximizeButton::ProcessUpdateEvent(const ui::LocatedEvent
& event
) {
335 DCHECK(is_snap_enabled_
);
336 if (!exceeded_drag_threshold_
) {
337 exceeded_drag_threshold_
= views::View::ExceededDragThreshold(
338 event
.location() - press_location_
);
340 if (exceeded_drag_threshold_
)
341 UpdateSnap(event
.location(), false, event
.IsGestureEvent());
344 bool FrameMaximizeButton::ProcessEndEvent(const ui::LocatedEvent
& event
) {
345 update_timer_
.Stop();
346 UninstallEventFilter();
347 bool should_snap
= is_snap_enabled_
;
348 is_snap_enabled_
= false;
350 // Remove our help bubble.
353 if (!should_snap
|| snap_type_
== SNAP_NONE
)
356 SetState(views::CustomButton::STATE_NORMAL
);
357 // SetState will not call SchedulePaint() if state was already set to
358 // STATE_NORMAL during a drag.
360 phantom_window_
.reset();
361 // Since Snap might destroy |this|, but the snap_sizer needs to be destroyed,
362 // The ownership of the snap_sizer is taken now.
363 scoped_ptr
<SnapSizer
> snap_sizer(snap_sizer_
.release());
364 Snap(*snap_sizer
.get());
368 void FrameMaximizeButton::Cancel(bool keep_menu_open
) {
369 if (!keep_menu_open
) {
371 UninstallEventFilter();
372 is_snap_enabled_
= false;
375 phantom_window_
.reset();
376 snap_type_
= SNAP_NONE
;
377 update_timer_
.Stop();
381 void FrameMaximizeButton::InstallEventFilter() {
382 if (escape_event_filter_
.get())
385 escape_event_filter_
.reset(new EscapeEventFilter(this));
388 void FrameMaximizeButton::UninstallEventFilter() {
389 escape_event_filter_
.reset(NULL
);
392 void FrameMaximizeButton::UpdateSnapFromEventLocation() {
393 // If the drag threshold has been exceeded the snap location is up to date.
394 if (exceeded_drag_threshold_
)
396 exceeded_drag_threshold_
= true;
397 UpdateSnap(press_location_
, false, press_is_gesture_
);
400 void FrameMaximizeButton::UpdateSnap(const gfx::Point
& location
,
403 SnapType type
= SnapTypeForLocation(location
);
404 if (type
== snap_type_
) {
405 if (snap_sizer_
.get()) {
406 snap_sizer_
->Update(LocationForSnapSizer(location
));
407 phantom_window_
->Show(ScreenAsh::ConvertRectToScreen(
408 frame_
->GetWidget()->GetNativeView()->parent(),
409 snap_sizer_
->target_bounds()));
418 if (snap_type_
== SNAP_NONE
) {
419 phantom_window_
.reset();
423 if (snap_type_
== SNAP_LEFT
|| snap_type_
== SNAP_RIGHT
) {
424 SnapSizer::Edge snap_edge
= snap_type_
== SNAP_LEFT
?
425 SnapSizer::LEFT_EDGE
: SnapSizer::RIGHT_EDGE
;
426 SnapSizer::InputType input_type
=
427 is_touch
? SnapSizer::TOUCH_MAXIMIZE_BUTTON_INPUT
:
428 SnapSizer::OTHER_INPUT
;
429 snap_sizer_
.reset(new SnapSizer(frame_
->GetWidget()->GetNativeWindow(),
430 LocationForSnapSizer(location
),
434 snap_sizer_
->SelectDefaultSizeAndDisableResize();
436 if (!phantom_window_
.get()) {
437 phantom_window_
.reset(new internal::PhantomWindowController(
438 frame_
->GetWidget()->GetNativeWindow()));
440 if (maximizer_
.get()) {
441 phantom_window_
->set_phantom_below_window(maximizer_
->GetBubbleWindow());
442 maximizer_
->SetSnapType(snap_type_
);
444 phantom_window_
->Show(
445 ScreenBoundsForType(snap_type_
, *snap_sizer_
.get()));
448 SnapType
FrameMaximizeButton::SnapTypeForLocation(
449 const gfx::Point
& location
) const {
450 MaximizeBubbleFrameState maximize_type
= GetMaximizeBubbleFrameState();
451 gfx::Vector2d
delta(location
- press_location_
);
452 if (!views::View::ExceededDragThreshold(delta
))
453 return maximize_type
!= FRAME_STATE_FULL
? SNAP_MAXIMIZE
: SNAP_RESTORE
;
454 if (delta
.x() < 0 && delta
.y() > delta
.x() && delta
.y() < -delta
.x())
455 return maximize_type
== FRAME_STATE_SNAP_LEFT
? SNAP_RESTORE
: SNAP_LEFT
;
456 if (delta
.x() > 0 && delta
.y() > -delta
.x() && delta
.y() < delta
.x())
457 return maximize_type
== FRAME_STATE_SNAP_RIGHT
? SNAP_RESTORE
: SNAP_RIGHT
;
459 return SNAP_MINIMIZE
;
460 return maximize_type
!= FRAME_STATE_FULL
? SNAP_MAXIMIZE
: SNAP_RESTORE
;
463 gfx::Rect
FrameMaximizeButton::ScreenBoundsForType(
465 const SnapSizer
& snap_sizer
) const {
466 aura::Window
* window
= frame_
->GetWidget()->GetNativeWindow();
470 return ScreenAsh::ConvertRectToScreen(
471 frame_
->GetWidget()->GetNativeView()->parent(),
472 snap_sizer
.target_bounds());
474 return ScreenAsh::ConvertRectToScreen(
476 ScreenAsh::GetMaximizedWindowBoundsInParent(window
));
477 case SNAP_MINIMIZE
: {
478 Launcher
* launcher
= Launcher::ForWindow(window
);
479 // Launcher is created lazily and can be NULL.
482 gfx::Rect
item_rect(launcher
->GetScreenBoundsOfItemIconForWindow(
484 if (!item_rect
.IsEmpty()) {
485 // PhantomWindowController insets slightly, outset it so the phantom
486 // doesn't appear inset.
487 item_rect
.Inset(-8, -8);
490 return launcher
->shelf_widget()->GetWindowBoundsInScreen();
493 const gfx::Rect
* restore
= GetRestoreBoundsInScreen(window
);
495 *restore
: frame_
->GetWidget()->GetWindowBoundsInScreen();
503 gfx::Point
FrameMaximizeButton::LocationForSnapSizer(
504 const gfx::Point
& location
) const {
505 gfx::Point
result(location
);
506 views::View::ConvertPointToScreen(this, &result
);
510 void FrameMaximizeButton::Snap(const SnapSizer
& snap_sizer
) {
511 ash::Shell
* shell
= ash::Shell::GetInstance();
512 views::Widget
* widget
= frame_
->GetWidget();
513 switch (snap_type_
) {
516 shell
->delegate()->RecordUserMetricsAction(
517 snap_type_
== SNAP_LEFT
? ash::UMA_MAXIMIZE_BUTTON_MAXIMIZE_LEFT
:
518 ash::UMA_MAXIMIZE_BUTTON_MAXIMIZE_RIGHT
);
519 // Get the bounds in screen coordinates for restore purposes.
520 gfx::Rect restore
= widget
->GetWindowBoundsInScreen();
521 if (widget
->IsMaximized() || widget
->IsFullscreen()) {
522 aura::Window
* window
= widget
->GetNativeWindow();
523 // In case of maximized we have a restore boundary.
524 DCHECK(ash::GetRestoreBoundsInScreen(window
));
525 // If it was maximized we need to recover the old restore set.
526 restore
= *ash::GetRestoreBoundsInScreen(window
);
528 // The auto position manager will kick in when this is the only window.
529 // To avoid interference with it we tell it temporarily to not change
530 // the coordinates of this window.
531 bool is_managed
= ash::wm::IsWindowPositionManaged(window
);
533 ash::wm::SetWindowPositionManaged(window
, false);
535 // Set the restore size we want to restore to.
536 ash::SetRestoreBoundsInScreen(window
,
537 ScreenBoundsForType(snap_type_
,
541 // After the window is where we want it to be we allow the window to be
542 // auto managed again.
544 ash::wm::SetWindowPositionManaged(window
, true);
546 // Others might also have set up a restore rectangle already. If so,
547 // we should not overwrite the restore rectangle.
549 GetRestoreBoundsInScreen(widget
->GetNativeWindow()) != NULL
;
550 widget
->SetBounds(ScreenBoundsForType(snap_type_
, snap_sizer
));
554 // Remember the widow's bounds for restoration.
555 ash::SetRestoreBoundsInScreen(widget
->GetNativeWindow(), restore
);
560 shell
->delegate()->RecordUserMetricsAction(
561 ash::UMA_MAXIMIZE_BUTTON_MAXIMIZE
);
565 shell
->delegate()->RecordUserMetricsAction(
566 ash::UMA_MAXIMIZE_BUTTON_MINIMIZE
);
570 shell
->delegate()->RecordUserMetricsAction(
571 ash::UMA_MAXIMIZE_BUTTON_RESTORE
);
578 MaximizeBubbleFrameState
579 FrameMaximizeButton::GetMaximizeBubbleFrameState() const {
580 // When there are no restore bounds, we are in normal mode.
581 if (!ash::GetRestoreBoundsInScreen(
582 frame_
->GetWidget()->GetNativeWindow()))
583 return FRAME_STATE_NONE
;
584 // The normal maximized test can be used.
585 if (frame_
->GetWidget()->IsMaximized())
586 return FRAME_STATE_FULL
;
587 // For Left/right maximize we need to check the dimensions.
588 gfx::Rect bounds
= frame_
->GetWidget()->GetWindowBoundsInScreen();
589 gfx::Rect screen
= Shell::GetScreen()->GetDisplayMatching(bounds
).work_area();
590 if (bounds
.width() < (screen
.width() * kMinSnapSizePercent
) / 100)
591 return FRAME_STATE_NONE
;
592 // We might still have a horizontally filled window at this point which we
593 // treat as no special state.
594 if (bounds
.y() != screen
.y() || bounds
.height() != screen
.height())
595 return FRAME_STATE_NONE
;
597 // We have to be in a maximize mode at this point.
598 if (bounds
.x() == screen
.x())
599 return FRAME_STATE_SNAP_LEFT
;
600 if (bounds
.right() == screen
.right())
601 return FRAME_STATE_SNAP_RIGHT
;
602 // If we come here, it is likely caused by the fact that the
603 // "VerticalResizeDoubleClick" stored a restore rectangle. In that case
604 // we allow all maximize operations (and keep the restore rectangle).
605 return FRAME_STATE_NONE
;