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
);
95 if (ash::Shell::IsForcedMaximizeMode())
96 views::View::SetVisible(false);
99 FrameMaximizeButton::~FrameMaximizeButton() {
100 // Before the window gets destroyed, the maximizer dialog needs to be shut
101 // down since it would otherwise call into a deleted object.
104 OnWindowDestroying(widget_
->GetNativeWindow());
107 void FrameMaximizeButton::SnapButtonHovered(SnapType type
) {
108 // Make sure to only show hover operations when no button is pressed and
109 // a similar snap operation in progress does not get re-applied.
110 if (is_snap_enabled_
|| (type
== snap_type_
&& snap_sizer_
))
112 // Prime the mouse location with the center of the (local) button.
113 press_location_
= gfx::Point(width() / 2, height() / 2);
114 // Then get an adjusted mouse position to initiate the effect.
115 gfx::Point location
= press_location_
;
118 location
.set_x(location
.x() - width());
121 location
.set_x(location
.x() + width());
124 location
.set_y(location
.y() + height());
127 // Simulate a mouse button move over the according button.
128 if (GetMaximizeBubbleFrameState() == FRAME_STATE_SNAP_LEFT
)
129 location
.set_x(location
.x() - width());
130 else if (GetMaximizeBubbleFrameState() == FRAME_STATE_SNAP_RIGHT
)
131 location
.set_x(location
.x() + width());
139 // We should not come here.
142 // Note: There is no hover with touch - we can therefore pass false for touch
144 UpdateSnap(location
, true, false);
147 void FrameMaximizeButton::ExecuteSnapAndCloseMenu(SnapType snap_type
) {
148 DCHECK_NE(snap_type_
, SNAP_NONE
);
150 // Tell our menu to close.
152 snap_type_
= snap_type
;
153 // Since Snap might destroy |this|, but the snap_sizer needs to be destroyed,
154 // The ownership of the snap_sizer is taken now.
155 scoped_ptr
<SnapSizer
> snap_sizer(snap_sizer_
.release());
156 Snap(*snap_sizer
.get());
159 void FrameMaximizeButton::DestroyMaximizeMenu() {
163 void FrameMaximizeButton::OnWindowBoundsChanged(
164 aura::Window
* window
,
165 const gfx::Rect
& old_bounds
,
166 const gfx::Rect
& new_bounds
) {
170 void FrameMaximizeButton::OnWindowPropertyChanged(aura::Window
* window
,
173 // Changing the window position is managed status should not Cancel.
174 // Note that this case might happen when a non user managed window
175 // transitions from maximized to L/R maximized.
176 if (key
!= ash::internal::kWindowPositionManagedKey
)
180 void FrameMaximizeButton::OnWindowDestroying(aura::Window
* window
) {
183 CHECK_EQ(widget_
->GetNativeWindow(), window
);
184 widget_
->GetNativeWindow()->RemoveObserver(this);
185 widget_
->RemoveObserver(this);
190 void FrameMaximizeButton::OnWidgetActivationChanged(views::Widget
* widget
,
192 // Upon losing focus, the control bubble should hide.
193 if (!active
&& maximizer_
)
197 bool FrameMaximizeButton::OnMousePressed(const ui::MouseEvent
& event
) {
198 // If we are already in a mouse click / drag operation, a second button down
199 // call will cancel (this addresses crbug.com/143755).
200 if (is_snap_enabled_
) {
203 is_snap_enabled_
= event
.IsOnlyLeftMouseButton();
204 if (is_snap_enabled_
)
205 ProcessStartEvent(event
);
207 ImageButton::OnMousePressed(event
);
211 void FrameMaximizeButton::OnMouseEntered(const ui::MouseEvent
& event
) {
212 ImageButton::OnMouseEntered(event
);
216 widget_
= frame_
->GetWidget();
217 widget_
->GetNativeWindow()->AddObserver(this);
218 widget_
->AddObserver(this);
220 maximizer_
.reset(new MaximizeBubbleController(
222 GetMaximizeBubbleFrameState(),
223 bubble_appearance_delay_ms_
));
227 void FrameMaximizeButton::OnMouseExited(const ui::MouseEvent
& event
) {
228 ImageButton::OnMouseExited(event
);
229 // Remove the bubble menu when the button is not pressed and the mouse is not
230 // within the bubble.
231 if (!is_snap_enabled_
&& maximizer_
) {
232 if (maximizer_
->GetBubbleWindow()) {
233 gfx::Point screen_location
= Shell::GetScreen()->GetCursorScreenPoint();
234 if (!maximizer_
->GetBubbleWindow()->GetBoundsInScreen().Contains(
237 // Make sure that all remaining snap hover states get removed.
238 SnapButtonHovered(SNAP_NONE
);
241 // The maximize dialog does not show up immediately after creating the
242 // |mazimizer_|. Destroy the dialog therefore before it shows up.
248 bool FrameMaximizeButton::OnMouseDragged(const ui::MouseEvent
& event
) {
249 if (is_snap_enabled_
)
250 ProcessUpdateEvent(event
);
251 return ImageButton::OnMouseDragged(event
);
254 void FrameMaximizeButton::OnMouseReleased(const ui::MouseEvent
& event
) {
256 bool snap_was_enabled
= is_snap_enabled_
;
257 if (!ProcessEndEvent(event
) && snap_was_enabled
)
258 ImageButton::OnMouseReleased(event
);
259 // At this point |this| might be already destroyed.
262 void FrameMaximizeButton::OnMouseCaptureLost() {
264 ImageButton::OnMouseCaptureLost();
267 void FrameMaximizeButton::OnGestureEvent(ui::GestureEvent
* event
) {
268 if (event
->type() == ui::ET_GESTURE_TAP_DOWN
) {
269 is_snap_enabled_
= true;
270 ProcessStartEvent(*event
);
275 if (event
->type() == ui::ET_GESTURE_TAP
||
276 (event
->type() == ui::ET_GESTURE_SCROLL_END
&& is_snap_enabled_
) ||
277 event
->type() == ui::ET_SCROLL_FLING_START
) {
278 // The position of the event may have changed from the previous event (both
279 // for TAP and SCROLL_END). So it is necessary to update the snap-state for
280 // the current event.
281 ProcessUpdateEvent(*event
);
282 if (event
->type() == ui::ET_GESTURE_TAP
)
283 snap_type_
= SnapTypeForLocation(event
->location());
284 ProcessEndEvent(*event
);
289 if (is_snap_enabled_
) {
290 if (event
->type() == ui::ET_GESTURE_END
&&
291 event
->details().touch_points() == 1) {
292 // The position of the event may have changed from the previous event. So
293 // it is necessary to update the snap-state for the current event.
294 ProcessUpdateEvent(*event
);
295 snap_type_
= SnapTypeForLocation(event
->location());
296 ProcessEndEvent(*event
);
301 if (event
->type() == ui::ET_GESTURE_SCROLL_UPDATE
||
302 event
->type() == ui::ET_GESTURE_SCROLL_BEGIN
) {
303 ProcessUpdateEvent(*event
);
309 ImageButton::OnGestureEvent(event
);
312 void FrameMaximizeButton::SetVisible(bool visible
) {
313 // In the enforced maximized mode we do not allow to be made visible.
314 if (ash::Shell::IsForcedMaximizeMode())
317 views::View::SetVisible(visible
);
320 void FrameMaximizeButton::ProcessStartEvent(const ui::LocatedEvent
& event
) {
321 DCHECK(is_snap_enabled_
);
322 // Prepare the help menu.
324 maximizer_
.reset(new MaximizeBubbleController(
326 GetMaximizeBubbleFrameState(),
327 bubble_appearance_delay_ms_
));
329 // If the menu did not show up yet, we delay it even a bit more.
330 maximizer_
->DelayCreation();
332 snap_sizer_
.reset(NULL
);
333 InstallEventFilter();
334 snap_type_
= SNAP_NONE
;
335 press_location_
= event
.location();
336 press_is_gesture_
= event
.IsGestureEvent();
337 exceeded_drag_threshold_
= false;
340 base::TimeDelta::FromMilliseconds(kUpdateDelayMS
),
342 &FrameMaximizeButton::UpdateSnapFromEventLocation
);
345 void FrameMaximizeButton::ProcessUpdateEvent(const ui::LocatedEvent
& event
) {
346 DCHECK(is_snap_enabled_
);
347 if (!exceeded_drag_threshold_
) {
348 exceeded_drag_threshold_
= views::View::ExceededDragThreshold(
349 event
.location() - press_location_
);
351 if (exceeded_drag_threshold_
)
352 UpdateSnap(event
.location(), false, event
.IsGestureEvent());
355 bool FrameMaximizeButton::ProcessEndEvent(const ui::LocatedEvent
& event
) {
356 update_timer_
.Stop();
357 UninstallEventFilter();
358 bool should_snap
= is_snap_enabled_
;
359 is_snap_enabled_
= false;
361 // Remove our help bubble.
364 if (!should_snap
|| snap_type_
== SNAP_NONE
)
367 SetState(views::CustomButton::STATE_NORMAL
);
368 // SetState will not call SchedulePaint() if state was already set to
369 // STATE_NORMAL during a drag.
371 phantom_window_
.reset();
372 // Since Snap might destroy |this|, but the snap_sizer needs to be destroyed,
373 // The ownership of the snap_sizer is taken now.
374 scoped_ptr
<SnapSizer
> snap_sizer(snap_sizer_
.release());
375 Snap(*snap_sizer
.get());
379 void FrameMaximizeButton::Cancel(bool keep_menu_open
) {
380 if (!keep_menu_open
) {
382 UninstallEventFilter();
383 is_snap_enabled_
= false;
386 phantom_window_
.reset();
387 snap_type_
= SNAP_NONE
;
388 update_timer_
.Stop();
392 void FrameMaximizeButton::InstallEventFilter() {
393 if (escape_event_filter_
)
396 escape_event_filter_
.reset(new EscapeEventFilter(this));
399 void FrameMaximizeButton::UninstallEventFilter() {
400 escape_event_filter_
.reset(NULL
);
403 void FrameMaximizeButton::UpdateSnapFromEventLocation() {
404 // If the drag threshold has been exceeded the snap location is up to date.
405 if (exceeded_drag_threshold_
)
407 exceeded_drag_threshold_
= true;
408 UpdateSnap(press_location_
, false, press_is_gesture_
);
411 void FrameMaximizeButton::UpdateSnap(const gfx::Point
& location
,
414 SnapType type
= SnapTypeForLocation(location
);
415 if (type
== snap_type_
) {
417 snap_sizer_
->Update(LocationForSnapSizer(location
));
418 phantom_window_
->Show(ScreenAsh::ConvertRectToScreen(
419 frame_
->GetWidget()->GetNativeView()->parent(),
420 snap_sizer_
->target_bounds()));
429 if (snap_type_
== SNAP_NONE
) {
430 phantom_window_
.reset();
434 if (snap_type_
== SNAP_LEFT
|| snap_type_
== SNAP_RIGHT
) {
435 SnapSizer::Edge snap_edge
= snap_type_
== SNAP_LEFT
?
436 SnapSizer::LEFT_EDGE
: SnapSizer::RIGHT_EDGE
;
437 SnapSizer::InputType input_type
=
438 is_touch
? SnapSizer::TOUCH_MAXIMIZE_BUTTON_INPUT
:
439 SnapSizer::OTHER_INPUT
;
440 snap_sizer_
.reset(new SnapSizer(frame_
->GetWidget()->GetNativeWindow(),
441 LocationForSnapSizer(location
),
445 snap_sizer_
->SelectDefaultSizeAndDisableResize();
447 if (!phantom_window_
) {
448 phantom_window_
.reset(new internal::PhantomWindowController(
449 frame_
->GetWidget()->GetNativeWindow()));
452 phantom_window_
->set_phantom_below_window(maximizer_
->GetBubbleWindow());
453 maximizer_
->SetSnapType(snap_type_
);
455 phantom_window_
->Show(
456 ScreenBoundsForType(snap_type_
, *snap_sizer_
.get()));
459 SnapType
FrameMaximizeButton::SnapTypeForLocation(
460 const gfx::Point
& location
) const {
461 MaximizeBubbleFrameState maximize_type
= GetMaximizeBubbleFrameState();
462 gfx::Vector2d
delta(location
- press_location_
);
463 if (!views::View::ExceededDragThreshold(delta
))
464 return maximize_type
!= FRAME_STATE_FULL
? SNAP_MAXIMIZE
: SNAP_RESTORE
;
465 if (delta
.x() < 0 && delta
.y() > delta
.x() && delta
.y() < -delta
.x())
466 return maximize_type
== FRAME_STATE_SNAP_LEFT
? SNAP_RESTORE
: SNAP_LEFT
;
467 if (delta
.x() > 0 && delta
.y() > -delta
.x() && delta
.y() < delta
.x())
468 return maximize_type
== FRAME_STATE_SNAP_RIGHT
? SNAP_RESTORE
: SNAP_RIGHT
;
470 return SNAP_MINIMIZE
;
471 return maximize_type
!= FRAME_STATE_FULL
? SNAP_MAXIMIZE
: SNAP_RESTORE
;
474 gfx::Rect
FrameMaximizeButton::ScreenBoundsForType(
476 const SnapSizer
& snap_sizer
) const {
477 aura::Window
* window
= frame_
->GetWidget()->GetNativeWindow();
481 return ScreenAsh::ConvertRectToScreen(
482 frame_
->GetWidget()->GetNativeView()->parent(),
483 snap_sizer
.target_bounds());
485 return ScreenAsh::ConvertRectToScreen(
487 ScreenAsh::GetMaximizedWindowBoundsInParent(window
));
488 case SNAP_MINIMIZE
: {
489 Launcher
* launcher
= Launcher::ForWindow(window
);
490 // Launcher is created lazily and can be NULL.
493 gfx::Rect
item_rect(launcher
->GetScreenBoundsOfItemIconForWindow(
495 if (!item_rect
.IsEmpty()) {
496 // PhantomWindowController insets slightly, outset it so the phantom
497 // doesn't appear inset.
498 item_rect
.Inset(-8, -8);
501 return launcher
->shelf_widget()->GetWindowBoundsInScreen();
504 const gfx::Rect
* restore
= GetRestoreBoundsInScreen(window
);
506 *restore
: frame_
->GetWidget()->GetWindowBoundsInScreen();
514 gfx::Point
FrameMaximizeButton::LocationForSnapSizer(
515 const gfx::Point
& location
) const {
516 gfx::Point
result(location
);
517 views::View::ConvertPointToScreen(this, &result
);
521 void FrameMaximizeButton::Snap(const SnapSizer
& snap_sizer
) {
522 ash::Shell
* shell
= ash::Shell::GetInstance();
523 views::Widget
* widget
= frame_
->GetWidget();
524 switch (snap_type_
) {
527 shell
->delegate()->RecordUserMetricsAction(
528 snap_type_
== SNAP_LEFT
?
529 ash::UMA_WINDOW_MAXIMIZE_BUTTON_MAXIMIZE_LEFT
:
530 ash::UMA_WINDOW_MAXIMIZE_BUTTON_MAXIMIZE_RIGHT
);
531 // Get the bounds in screen coordinates for restore purposes.
532 gfx::Rect restore
= widget
->GetWindowBoundsInScreen();
533 if (widget
->IsMaximized() || widget
->IsFullscreen()) {
534 aura::Window
* window
= widget
->GetNativeWindow();
535 // In case of maximized we have a restore boundary.
536 DCHECK(ash::GetRestoreBoundsInScreen(window
));
537 // If it was maximized we need to recover the old restore set.
538 restore
= *ash::GetRestoreBoundsInScreen(window
);
540 // The auto position manager will kick in when this is the only window.
541 // To avoid interference with it we tell it temporarily to not change
542 // the coordinates of this window.
543 bool is_managed
= ash::wm::IsWindowPositionManaged(window
);
545 ash::wm::SetWindowPositionManaged(window
, false);
547 // Set the restore size we want to restore to.
548 ash::SetRestoreBoundsInScreen(window
,
549 ScreenBoundsForType(snap_type_
,
553 // After the window is where we want it to be we allow the window to be
554 // auto managed again.
556 ash::wm::SetWindowPositionManaged(window
, true);
558 // Others might also have set up a restore rectangle already. If so,
559 // we should not overwrite the restore rectangle.
561 GetRestoreBoundsInScreen(widget
->GetNativeWindow()) != NULL
;
562 widget
->SetBounds(ScreenBoundsForType(snap_type_
, snap_sizer
));
566 // Remember the widow's bounds for restoration.
567 ash::SetRestoreBoundsInScreen(widget
->GetNativeWindow(), restore
);
572 shell
->delegate()->RecordUserMetricsAction(
573 ash::UMA_WINDOW_MAXIMIZE_BUTTON_MAXIMIZE
);
577 shell
->delegate()->RecordUserMetricsAction(
578 ash::UMA_WINDOW_MAXIMIZE_BUTTON_MINIMIZE
);
582 shell
->delegate()->RecordUserMetricsAction(
583 ash::UMA_WINDOW_MAXIMIZE_BUTTON_RESTORE
);
590 MaximizeBubbleFrameState
591 FrameMaximizeButton::GetMaximizeBubbleFrameState() const {
592 // When there are no restore bounds, we are in normal mode.
593 if (!ash::GetRestoreBoundsInScreen(
594 frame_
->GetWidget()->GetNativeWindow()))
595 return FRAME_STATE_NONE
;
596 // The normal maximized test can be used.
597 if (frame_
->GetWidget()->IsMaximized())
598 return FRAME_STATE_FULL
;
599 // For Left/right maximize we need to check the dimensions.
600 gfx::Rect bounds
= frame_
->GetWidget()->GetWindowBoundsInScreen();
601 gfx::Rect screen
= Shell::GetScreen()->GetDisplayMatching(bounds
).work_area();
602 if (bounds
.width() < (screen
.width() * kMinSnapSizePercent
) / 100)
603 return FRAME_STATE_NONE
;
604 // We might still have a horizontally filled window at this point which we
605 // treat as no special state.
606 if (bounds
.y() != screen
.y() || bounds
.height() != screen
.height())
607 return FRAME_STATE_NONE
;
609 // We have to be in a maximize mode at this point.
610 if (bounds
.x() == screen
.x())
611 return FRAME_STATE_SNAP_LEFT
;
612 if (bounds
.right() == screen
.right())
613 return FRAME_STATE_SNAP_RIGHT
;
614 // If we come here, it is likely caused by the fact that the
615 // "VerticalResizeDoubleClick" stored a restore rectangle. In that case
616 // we allow all maximize operations (and keep the restore rectangle).
617 return FRAME_STATE_NONE
;