Separate Simple Backend creation from initialization.
[chromium-blink-merge.git] / ash / wm / workspace / frame_maximize_button.cc
blobc7dd3bd5affd7130460be4a835213119ad370da5
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;
31 namespace ash {
33 namespace {
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 {
49 public:
50 explicit EscapeEventFilter(FrameMaximizeButton* button);
51 virtual ~EscapeEventFilter();
53 // EventFilter overrides:
54 virtual void OnKeyEvent(ui::KeyEvent* event) OVERRIDE;
56 private:
57 FrameMaximizeButton* button_;
59 DISALLOW_COPY_AND_ASSIGN(EscapeEventFilter);
62 FrameMaximizeButton::EscapeEventFilter::EscapeEventFilter(
63 FrameMaximizeButton* button)
64 : button_(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),
85 frame_(frame),
86 is_snap_enabled_(false),
87 exceeded_drag_threshold_(false),
88 widget_(NULL),
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.
99 maximizer_.reset();
100 if (widget_)
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()))
108 return;
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_;
113 switch (type) {
114 case SNAP_LEFT:
115 location.set_x(location.x() - width());
116 break;
117 case SNAP_RIGHT:
118 location.set_x(location.x() + width());
119 break;
120 case SNAP_MINIMIZE:
121 location.set_y(location.y() + height());
122 break;
123 case SNAP_RESTORE:
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());
129 break;
130 case SNAP_MAXIMIZE:
131 break;
132 case SNAP_NONE:
133 Cancel(true);
134 return;
135 default:
136 // We should not come here.
137 NOTREACHED();
139 // Note: There is no hover with touch - we can therefore pass false for touch
140 // operations.
141 UpdateSnap(location, true, false);
144 void FrameMaximizeButton::ExecuteSnapAndCloseMenu(SnapType snap_type) {
145 DCHECK_NE(snap_type_, SNAP_NONE);
146 Cancel(true);
147 // Tell our menu to close.
148 maximizer_.reset();
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() {
157 Cancel(false);
160 void FrameMaximizeButton::OnWindowBoundsChanged(
161 aura::Window* window,
162 const gfx::Rect& old_bounds,
163 const gfx::Rect& new_bounds) {
164 Cancel(false);
167 void FrameMaximizeButton::OnWindowPropertyChanged(aura::Window* window,
168 const void* key,
169 intptr_t old) {
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)
174 Cancel(false);
177 void FrameMaximizeButton::OnWindowDestroying(aura::Window* window) {
178 maximizer_.reset();
179 if (widget_) {
180 CHECK_EQ(widget_->GetNativeWindow(), window);
181 widget_->GetNativeWindow()->RemoveObserver(this);
182 widget_->RemoveObserver(this);
183 widget_ = NULL;
187 void FrameMaximizeButton::OnWidgetActivationChanged(views::Widget* widget,
188 bool active) {
189 // Upon losing focus, the control bubble should hide.
190 if (!active && maximizer_.get())
191 maximizer_.reset();
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_) {
198 Cancel(false);
199 } else {
200 is_snap_enabled_ = event.IsOnlyLeftMouseButton();
201 if (is_snap_enabled_)
202 ProcessStartEvent(event);
204 ImageButton::OnMousePressed(event);
205 return true;
208 void FrameMaximizeButton::OnMouseEntered(const ui::MouseEvent& event) {
209 ImageButton::OnMouseEntered(event);
210 if (!maximizer_.get()) {
211 DCHECK(GetWidget());
212 if (!widget_) {
213 widget_ = frame_->GetWidget();
214 widget_->GetNativeWindow()->AddObserver(this);
215 widget_->AddObserver(this);
217 maximizer_.reset(new MaximizeBubbleController(
218 this,
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(
232 screen_location)) {
233 maximizer_.reset();
234 // Make sure that all remaining snap hover states get removed.
235 SnapButtonHovered(SNAP_NONE);
237 } else {
238 // The maximize dialog does not show up immediately after creating the
239 // |mazimizer_|. Destroy the dialog therefore before it shows up.
240 maximizer_.reset();
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) {
252 maximizer_.reset();
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() {
260 Cancel(false);
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);
268 event->SetHandled();
269 return;
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);
282 event->SetHandled();
283 return;
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);
294 event->SetHandled();
295 return;
298 if (event->type() == ui::ET_GESTURE_SCROLL_UPDATE ||
299 event->type() == ui::ET_GESTURE_SCROLL_BEGIN) {
300 ProcessUpdateEvent(*event);
301 event->SetHandled();
302 return;
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(
314 this,
315 GetMaximizeBubbleFrameState(),
316 bubble_appearance_delay_ms_));
317 } else {
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;
327 update_timer_.Start(
328 FROM_HERE,
329 base::TimeDelta::FromMilliseconds(kUpdateDelayMS),
330 this,
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.
351 maximizer_.reset();
353 if (!should_snap || snap_type_ == SNAP_NONE)
354 return false;
356 SetState(views::CustomButton::STATE_NORMAL);
357 // SetState will not call SchedulePaint() if state was already set to
358 // STATE_NORMAL during a drag.
359 SchedulePaint();
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());
365 return true;
368 void FrameMaximizeButton::Cancel(bool keep_menu_open) {
369 if (!keep_menu_open) {
370 maximizer_.reset();
371 UninstallEventFilter();
372 is_snap_enabled_ = false;
373 snap_sizer_.reset();
375 phantom_window_.reset();
376 snap_type_ = SNAP_NONE;
377 update_timer_.Stop();
378 SchedulePaint();
381 void FrameMaximizeButton::InstallEventFilter() {
382 if (escape_event_filter_.get())
383 return;
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_)
395 return;
396 exceeded_drag_threshold_ = true;
397 UpdateSnap(press_location_, false, press_is_gesture_);
400 void FrameMaximizeButton::UpdateSnap(const gfx::Point& location,
401 bool select_default,
402 bool is_touch) {
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()));
411 return;
414 snap_type_ = type;
415 snap_sizer_.reset();
416 SchedulePaint();
418 if (snap_type_ == SNAP_NONE) {
419 phantom_window_.reset();
420 return;
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),
431 snap_edge,
432 input_type));
433 if (select_default)
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;
458 if (delta.y() > 0)
459 return SNAP_MINIMIZE;
460 return maximize_type != FRAME_STATE_FULL ? SNAP_MAXIMIZE : SNAP_RESTORE;
463 gfx::Rect FrameMaximizeButton::ScreenBoundsForType(
464 SnapType type,
465 const SnapSizer& snap_sizer) const {
466 aura::Window* window = frame_->GetWidget()->GetNativeWindow();
467 switch (type) {
468 case SNAP_LEFT:
469 case SNAP_RIGHT:
470 return ScreenAsh::ConvertRectToScreen(
471 frame_->GetWidget()->GetNativeView()->parent(),
472 snap_sizer.target_bounds());
473 case SNAP_MAXIMIZE:
474 return ScreenAsh::ConvertRectToScreen(
475 window->parent(),
476 ScreenAsh::GetMaximizedWindowBoundsInParent(window));
477 case SNAP_MINIMIZE: {
478 Launcher* launcher = Launcher::ForWindow(window);
479 // Launcher is created lazily and can be NULL.
480 if (!launcher)
481 return gfx::Rect();
482 gfx::Rect item_rect(launcher->GetScreenBoundsOfItemIconForWindow(
483 window));
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);
488 return item_rect;
490 return launcher->shelf_widget()->GetWindowBoundsInScreen();
492 case SNAP_RESTORE: {
493 const gfx::Rect* restore = GetRestoreBoundsInScreen(window);
494 return restore ?
495 *restore : frame_->GetWidget()->GetWindowBoundsInScreen();
497 case SNAP_NONE:
498 NOTREACHED();
500 return gfx::Rect();
503 gfx::Point FrameMaximizeButton::LocationForSnapSizer(
504 const gfx::Point& location) const {
505 gfx::Point result(location);
506 views::View::ConvertPointToScreen(this, &result);
507 return 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_) {
514 case SNAP_LEFT:
515 case SNAP_RIGHT: {
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);
532 if (is_managed)
533 ash::wm::SetWindowPositionManaged(window, false);
535 // Set the restore size we want to restore to.
536 ash::SetRestoreBoundsInScreen(window,
537 ScreenBoundsForType(snap_type_,
538 snap_sizer));
539 widget->Restore();
541 // After the window is where we want it to be we allow the window to be
542 // auto managed again.
543 if (is_managed)
544 ash::wm::SetWindowPositionManaged(window, true);
545 } else {
546 // Others might also have set up a restore rectangle already. If so,
547 // we should not overwrite the restore rectangle.
548 bool restore_set =
549 GetRestoreBoundsInScreen(widget->GetNativeWindow()) != NULL;
550 widget->SetBounds(ScreenBoundsForType(snap_type_, snap_sizer));
551 if (restore_set)
552 break;
554 // Remember the widow's bounds for restoration.
555 ash::SetRestoreBoundsInScreen(widget->GetNativeWindow(), restore);
556 break;
558 case SNAP_MAXIMIZE:
559 widget->Maximize();
560 shell->delegate()->RecordUserMetricsAction(
561 ash::UMA_MAXIMIZE_BUTTON_MAXIMIZE);
562 break;
563 case SNAP_MINIMIZE:
564 widget->Minimize();
565 shell->delegate()->RecordUserMetricsAction(
566 ash::UMA_MAXIMIZE_BUTTON_MINIMIZE);
567 break;
568 case SNAP_RESTORE:
569 widget->Restore();
570 shell->delegate()->RecordUserMetricsAction(
571 ash::UMA_MAXIMIZE_BUTTON_RESTORE);
572 break;
573 case SNAP_NONE:
574 NOTREACHED();
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;
608 } // namespace ash