1// Copyright 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.
4
5#include "chrome/browser/ui/views/frame/immersive_mode_controller_ash.h"
6
7#include <set>
8#include <vector>
9
10#include "ash/ash_switches.h"
11#include "ash/shell.h"
12#include "ash/wm/window_properties.h"
13#include "base/command_line.h"
14#include "chrome/browser/chrome_notification_types.h"
15#include "chrome/browser/ui/fullscreen/fullscreen_controller.h"
16#include "chrome/browser/ui/immersive_fullscreen_configuration.h"
17#include "chrome/browser/ui/views/bookmarks/bookmark_bar_view.h"
18#include "chrome/browser/ui/views/frame/top_container_view.h"
19#include "content/public/browser/notification_service.h"
20#include "content/public/browser/web_contents.h"
21#include "content/public/browser/web_contents_view.h"
22#include "ui/aura/client/activation_client.h"
23#include "ui/aura/client/aura_constants.h"
24#include "ui/aura/client/capture_client.h"
25#include "ui/aura/client/cursor_client.h"
26#include "ui/aura/client/screen_position_client.h"
27#include "ui/aura/env.h"
28#include "ui/aura/root_window.h"
29#include "ui/aura/window.h"
30#include "ui/base/animation/slide_animation.h"
31#include "ui/views/bubble/bubble_delegate.h"
32#include "ui/views/view.h"
33#include "ui/views/widget/widget.h"
34#include "ui/views/window/non_client_view.h"
35
36using views::View;
37
38namespace {
39
40// The slide open/closed animation looks better if it starts and ends just a
41// few pixels before the view goes completely off the screen, which reduces
42// the visual "pop" as the 2-pixel tall immersive-style tabs become visible.
43const int kAnimationOffsetY = 3;
44
45// Duration for the reveal show/hide slide animation. The slower duration is
46// used for the initial slide out to give the user more change to see what
47// happened.
48const int kRevealSlowAnimationDurationMs = 400;
49const int kRevealFastAnimationDurationMs = 200;
50
51// How many pixels a gesture can start away from |top_container_| when in
52// closed state and still be considered near it. This is needed to overcome
53// issues with poor location values near the edge of the display.
54const int kNearTopContainerDistance = 8;
55
56// Used to multiply x value of an update in check to determine if gesture is
57// vertical. This is used to make sure that gesture is close to vertical instead
58// of just more vertical then horizontal.
59const int kSwipeVerticalThresholdMultiplier = 3;
60
61// The height in pixels of the region above the top edge of the display which
62// hosts the immersive fullscreen window in which mouse events are ignored
63// (cannot reveal or unreveal the top-of-window views).
64// See ShouldIgnoreMouseEventAtLocation() for more details.
65const int kHeightOfDeadRegionAboveTopContainer = 10;
66
67// The height in pixels of the region below the top edge of the display in which
68// the mouse can trigger revealing the top-of-window views. The height must be
69// greater than 1px because the top pixel is used to trigger moving the cursor
70// between displays if the user has a vertical display layout (primary display
71// above/below secondary display).
72const int kMouseRevealBoundsHeight = 3;
73
74// If |hovered| is true, moves the mouse above |view|. Moves it outside of
75// |view| otherwise.
76// Should not be called outside of tests.
77void MoveMouse(views::View* view, bool hovered) {
78  gfx::Point cursor_pos;
79  if (!hovered) {
80    int bottom_edge = view->bounds().bottom();
81    cursor_pos = gfx::Point(0, bottom_edge + 100);
82  }
83  views::View::ConvertPointToScreen(view, &cursor_pos);
84  aura::Env::GetInstance()->set_last_mouse_location(cursor_pos);
85}
86
87// Returns the BubbleDelegateView corresponding to |maybe_bubble| if
88// |maybe_bubble| is a bubble.
89views::BubbleDelegateView* AsBubbleDelegate(aura::Window* maybe_bubble) {
90  if (!maybe_bubble)
91    return NULL;
92  views::Widget* widget = views::Widget::GetWidgetForNativeView(maybe_bubble);
93  if (!widget)
94    return NULL;
95  return widget->widget_delegate()->AsBubbleDelegate();
96}
97
98// Returns true if |maybe_transient| is a transient child of |toplevel|.
99bool IsWindowTransientChildOf(aura::Window* maybe_transient,
100                              aura::Window* toplevel) {
101  if (!maybe_transient || !toplevel)
102    return false;
103
104  for (aura::Window* window = maybe_transient; window;
105       window = window->transient_parent()) {
106    if (window == toplevel)
107      return true;
108  }
109  return false;
110}
111
112// Returns the location of |event| in screen coordinates.
113gfx::Point GetEventLocationInScreen(const ui::LocatedEvent& event) {
114  gfx::Point location_in_screen = event.location();
115  aura::Window* target = static_cast<aura::Window*>(event.target());
116  aura::client::ScreenPositionClient* screen_position_client =
117      aura::client::GetScreenPositionClient(target->GetRootWindow());
118  screen_position_client->ConvertPointToScreen(target, &location_in_screen);
119  return location_in_screen;
120}
121
122////////////////////////////////////////////////////////////////////////////////
123
124class RevealedLockAsh : public ImmersiveRevealedLock {
125 public:
126  RevealedLockAsh(const base::WeakPtr<ImmersiveModeControllerAsh>& controller,
127                  ImmersiveModeController::AnimateReveal animate_reveal)
128      : controller_(controller) {
129    DCHECK(controller_);
130    controller_->LockRevealedState(animate_reveal);
131  }
132
133  virtual ~RevealedLockAsh() {
134    if (controller_)
135      controller_->UnlockRevealedState();
136  }
137
138 private:
139  base::WeakPtr<ImmersiveModeControllerAsh> controller_;
140
141  DISALLOW_COPY_AND_ASSIGN(RevealedLockAsh);
142};
143
144}  // namespace
145
146////////////////////////////////////////////////////////////////////////////////
147
148// Class which keeps the top-of-window views revealed as long as one of the
149// bubbles it is observing is visible. The logic to keep the top-of-window
150// views revealed based on the visibility of bubbles anchored to
151// children of |ImmersiveModeController::top_container_| is separate from
152// the logic related to |ImmersiveModeControllerAsh::focus_revealed_lock_|
153// so that bubbles which are not activatable and bubbles which do not close
154// upon deactivation also keep the top-of-window views revealed for the
155// duration of their visibility.
156class ImmersiveModeControllerAsh::BubbleManager : public aura::WindowObserver {
157 public:
158  explicit BubbleManager(ImmersiveModeControllerAsh* controller);
159  virtual ~BubbleManager();
160
161  // Start / stop observing changes to |bubble|'s visibility.
162  void StartObserving(aura::Window* bubble);
163  void StopObserving(aura::Window* bubble);
164
165 private:
166  // Updates |revealed_lock_| based on whether any of |bubbles_| is visible.
167  void UpdateRevealedLock();
168
169  // aura::WindowObserver overrides:
170  virtual void OnWindowVisibilityChanged(aura::Window* window,
171                                         bool visible) OVERRIDE;
172  virtual void OnWindowDestroying(aura::Window* window) OVERRIDE;
173
174  ImmersiveModeControllerAsh* controller_;
175
176  std::set<aura::Window*> bubbles_;
177
178  // Lock which keeps the top-of-window views revealed based on whether any of
179  // |bubbles_| is visible.
180  scoped_ptr<ImmersiveRevealedLock> revealed_lock_;
181
182  DISALLOW_COPY_AND_ASSIGN(BubbleManager);
183};
184
185ImmersiveModeControllerAsh::BubbleManager::BubbleManager(
186    ImmersiveModeControllerAsh* controller)
187    : controller_(controller) {
188}
189
190ImmersiveModeControllerAsh::BubbleManager::~BubbleManager() {
191  for (std::set<aura::Window*>::const_iterator it = bubbles_.begin();
192       it != bubbles_.end(); ++it) {
193    (*it)->RemoveObserver(this);
194  }
195}
196
197void ImmersiveModeControllerAsh::BubbleManager::StartObserving(
198    aura::Window* bubble) {
199  if (bubbles_.insert(bubble).second) {
200    bubble->AddObserver(this);
201    UpdateRevealedLock();
202  }
203}
204
205void ImmersiveModeControllerAsh::BubbleManager::StopObserving(
206    aura::Window* bubble) {
207  if (bubbles_.erase(bubble)) {
208    bubble->RemoveObserver(this);
209    UpdateRevealedLock();
210  }
211}
212
213void ImmersiveModeControllerAsh::BubbleManager::UpdateRevealedLock() {
214  bool has_visible_bubble = false;
215  for (std::set<aura::Window*>::const_iterator it = bubbles_.begin();
216       it != bubbles_.end(); ++it) {
217    if ((*it)->IsVisible()) {
218      has_visible_bubble = true;
219      break;
220    }
221  }
222
223  bool was_revealed = controller_->IsRevealed();
224  if (has_visible_bubble) {
225    if (!revealed_lock_.get()) {
226      // Reveal the top-of-window views without animating because it looks
227      // weird for the top-of-window views to animate and the bubble not to
228      // animate along with the top-of-window views.
229      revealed_lock_.reset(controller_->GetRevealedLock(
230          ImmersiveModeController::ANIMATE_REVEAL_NO));
231    }
232  } else {
233    revealed_lock_.reset();
234  }
235
236  if (!was_revealed && revealed_lock_.get()) {
237    // Currently, there is no nice way for bubbles to reposition themselves
238    // whenever the anchor view moves. Tell the bubbles to reposition themselves
239    // explicitly instead. The hidden bubbles are also repositioned because
240    // BubbleDelegateView does not reposition its widget as a result of a
241    // visibility change.
242    for (std::set<aura::Window*>::const_iterator it = bubbles_.begin();
243         it != bubbles_.end(); ++it) {
244      AsBubbleDelegate(*it)->OnAnchorViewBoundsChanged();
245    }
246  }
247}
248
249void ImmersiveModeControllerAsh::BubbleManager::OnWindowVisibilityChanged(
250    aura::Window*,
251    bool visible) {
252  UpdateRevealedLock();
253}
254
255void ImmersiveModeControllerAsh::BubbleManager::OnWindowDestroying(
256    aura::Window* window) {
257  StopObserving(window);
258}
259
260////////////////////////////////////////////////////////////////////////////////
261
262ImmersiveModeControllerAsh::ImmersiveModeControllerAsh()
263    : delegate_(NULL),
264      widget_(NULL),
265      top_container_(NULL),
266      observers_enabled_(false),
267      enabled_(false),
268      reveal_state_(CLOSED),
269      revealed_lock_count_(0),
270      tab_indicator_visibility_(TAB_INDICATORS_HIDE),
271      mouse_x_when_hit_top_in_screen_(-1),
272      gesture_begun_(false),
273      native_window_(NULL),
274      animation_(new ui::SlideAnimation(this)),
275      animations_disabled_for_test_(false),
276      weak_ptr_factory_(this) {
277}
278
279ImmersiveModeControllerAsh::~ImmersiveModeControllerAsh() {
280  // The browser view is being destroyed so there's no need to update its
281  // layout or layers, even if the top views are revealed. But the window
282  // observers still need to be removed.
283  EnableWindowObservers(false);
284}
285
286void ImmersiveModeControllerAsh::LockRevealedState(
287      AnimateReveal animate_reveal) {
288  ++revealed_lock_count_;
289  Animate animate = (animate_reveal == ANIMATE_REVEAL_YES) ?
290      ANIMATE_FAST : ANIMATE_NO;
291  MaybeStartReveal(animate);
292}
293
294void ImmersiveModeControllerAsh::UnlockRevealedState() {
295  --revealed_lock_count_;
296  DCHECK_GE(revealed_lock_count_, 0);
297  if (revealed_lock_count_ == 0) {
298    // Always animate ending the reveal fast.
299    MaybeEndReveal(ANIMATE_FAST);
300  }
301}
302
303void ImmersiveModeControllerAsh::MaybeExitImmersiveFullscreen() {
304  if (ShouldExitImmersiveFullscreen())
305    delegate_->FullscreenStateChanged();
306}
307
308void ImmersiveModeControllerAsh::Init(
309    Delegate* delegate,
310    views::Widget* widget,
311    views::View* top_container) {
312  delegate_ = delegate;
313  widget_ = widget;
314  // Browser view is detached from its widget during destruction. Cache the
315  // window pointer so |this| can stop observing during destruction.
316  native_window_ = widget_->GetNativeWindow();
317  top_container_ = top_container;
318
319  // Optionally allow the tab indicators to be hidden.
320  if (CommandLine::ForCurrentProcess()->
321          HasSwitch(ash::switches::kAshImmersiveHideTabIndicators)) {
322    tab_indicator_visibility_ = TAB_INDICATORS_FORCE_HIDE;
323  }
324}
325
326void ImmersiveModeControllerAsh::SetEnabled(bool enabled) {
327  DCHECK(native_window_) << "Must initialize before enabling";
328  if (enabled_ == enabled)
329    return;
330  enabled_ = enabled;
331
332  EnableWindowObservers(enabled_);
333
334  UpdateUseMinimalChrome(LAYOUT_NO);
335
336  if (enabled_) {
337    // Animate enabling immersive mode by sliding out the top-of-window views.
338    // No animation occurs if a lock is holding the top-of-window views open.
339
340    // Do a reveal to set the initial state for the animation. (And any
341    // required state in case the animation cannot run because of a lock holding
342    // the top-of-window views open.) This call has the side effect of relaying
343    // out |browser_view_|'s root view.
344    MaybeStartReveal(ANIMATE_NO);
345
346    // Reset the located event and the focus revealed locks so that they do not
347    // affect whether the top-of-window views are hidden.
348    located_event_revealed_lock_.reset();
349    focus_revealed_lock_.reset();
350
351    // Try doing the animation.
352    MaybeEndReveal(ANIMATE_SLOW);
353
354    if (reveal_state_ == REVEALED) {
355      // Reveal was unsuccessful. Reacquire the revealed locks if appropriate.
356      UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_NO);
357      UpdateFocusRevealedLock();
358    }
359  } else {
360    // Stop cursor-at-top tracking.
361    top_edge_hover_timer_.Stop();
362    // Snap immediately to the closed state.
363    reveal_state_ = CLOSED;
364    EnablePaintToLayer(false);
365    delegate_->SetImmersiveStyle(false);
366    SetRenderWindowTopInsetsForTouch(0);
367
368    // Relayout the root view because disabling immersive fullscreen may have
369    // changed the result of NonClientFrameView::GetBoundsForClientView().
370    LayoutBrowserRootView();
371  }
372}
373
374bool ImmersiveModeControllerAsh::IsEnabled() const {
375  return enabled_;
376}
377
378bool ImmersiveModeControllerAsh::ShouldHideTabIndicators() const {
379  return tab_indicator_visibility_ != TAB_INDICATORS_SHOW;
380}
381
382bool ImmersiveModeControllerAsh::ShouldHideTopViews() const {
383  return enabled_ && reveal_state_ == CLOSED;
384}
385
386bool ImmersiveModeControllerAsh::IsRevealed() const {
387  return enabled_ && reveal_state_ != CLOSED;
388}
389
390int ImmersiveModeControllerAsh::GetTopContainerVerticalOffset(
391    const gfx::Size& top_container_size) const {
392  if (!enabled_ || reveal_state_ == REVEALED || reveal_state_ == CLOSED)
393    return 0;
394
395  return animation_->CurrentValueBetween(
396      -top_container_size.height() + kAnimationOffsetY, 0);
397}
398
399ImmersiveRevealedLock* ImmersiveModeControllerAsh::GetRevealedLock(
400    AnimateReveal animate_reveal) {
401  return new RevealedLockAsh(weak_ptr_factory_.GetWeakPtr(), animate_reveal);
402}
403
404void ImmersiveModeControllerAsh::OnFindBarVisibleBoundsChanged(
405    const gfx::Rect& new_visible_bounds_in_screen) {
406  find_bar_visible_bounds_in_screen_ = new_visible_bounds_in_screen;
407}
408
409////////////////////////////////////////////////////////////////////////////////
410// Observers:
411
412void ImmersiveModeControllerAsh::Observe(
413    int type,
414    const content::NotificationSource& source,
415    const content::NotificationDetails& details) {
416  DCHECK_EQ(chrome::NOTIFICATION_FULLSCREEN_CHANGED, type);
417  if (enabled_)
418    UpdateUseMinimalChrome(LAYOUT_YES);
419}
420
421void ImmersiveModeControllerAsh::OnMouseEvent(ui::MouseEvent* event) {
422  if (!enabled_)
423    return;
424
425  if (event->type() != ui::ET_MOUSE_MOVED &&
426      event->type() != ui::ET_MOUSE_PRESSED &&
427      event->type() != ui::ET_MOUSE_RELEASED &&
428      event->type() != ui::ET_MOUSE_CAPTURE_CHANGED) {
429    return;
430  }
431
432  // Mouse hover should not initiate revealing the top-of-window views while
433  // |native_window_| is inactive.
434  if (!views::Widget::GetWidgetForNativeWindow(native_window_)->IsActive())
435    return;
436
437  // Mouse hover should not initiate revealing the top-of-window views while
438  // a window has mouse capture.
439  if (aura::client::GetCaptureWindow(native_window_))
440    return;
441
442  if (IsRevealed())
443    UpdateLocatedEventRevealedLock(event, ALLOW_REVEAL_WHILE_CLOSING_NO);
444
445  // Trigger a reveal if the cursor pauses at the top of the screen for a
446  // while.
447  if (event->type() != ui::ET_MOUSE_CAPTURE_CHANGED)
448    UpdateTopEdgeHoverTimer(event);
449}
450
451void ImmersiveModeControllerAsh::OnTouchEvent(ui::TouchEvent* event) {
452  if (!enabled_ || event->type() != ui::ET_TOUCH_PRESSED)
453    return;
454
455  UpdateLocatedEventRevealedLock(event, ALLOW_REVEAL_WHILE_CLOSING_NO);
456}
457
458void ImmersiveModeControllerAsh::OnGestureEvent(ui::GestureEvent* event) {
459  if (!enabled_)
460    return;
461
462  // Touch gestures should not initiate revealing the top-of-window views while
463  // |native_window_| is inactive.
464  if (!views::Widget::GetWidgetForNativeWindow(native_window_)->IsActive())
465    return;
466
467  switch (event->type()) {
468    case ui::ET_GESTURE_SCROLL_BEGIN:
469      if (ShouldHandleGestureEvent(GetEventLocationInScreen(*event))) {
470        gesture_begun_ = true;
471        event->SetHandled();
472      }
473      break;
474    case ui::ET_GESTURE_SCROLL_UPDATE:
475      if (gesture_begun_) {
476        if (UpdateRevealedLocksForSwipe(GetSwipeType(event)))
477          event->SetHandled();
478        gesture_begun_ = false;
479      }
480      break;
481    case ui::ET_GESTURE_SCROLL_END:
482    case ui::ET_SCROLL_FLING_START:
483      gesture_begun_ = false;
484      break;
485    default:
486      break;
487  }
488}
489
490void ImmersiveModeControllerAsh::OnWillChangeFocus(views::View* focused_before,
491                                                   views::View* focused_now) {
492}
493
494void ImmersiveModeControllerAsh::OnDidChangeFocus(views::View* focused_before,
495                                                  views::View* focused_now) {
496  UpdateFocusRevealedLock();
497}
498
499void ImmersiveModeControllerAsh::OnWidgetDestroying(views::Widget* widget) {
500  EnableWindowObservers(false);
501  native_window_ = NULL;
502
503  // Set |enabled_| to false such that any calls to MaybeStartReveal() and
504  // MaybeEndReveal() have no effect.
505  enabled_ = false;
506}
507
508void ImmersiveModeControllerAsh::OnWidgetActivationChanged(
509    views::Widget* widget,
510    bool active) {
511  // Mouse hover should not initiate revealing the top-of-window views while
512  // |native_window_| is inactive.
513  top_edge_hover_timer_.Stop();
514
515  UpdateFocusRevealedLock();
516
517  // Allow the top-of-window views to stay revealed if all of the revealed locks
518  // were released in the process of activating |widget| but the mouse is still
519  // hovered above the top-of-window views. For instance, if the bubble which
520  // has been keeping the top-of-window views revealed is hidden but the mouse
521  // is hovered above the top-of-window views, the top-of-window views should
522  // stay revealed. We cannot call UpdateLocatedEventRevealedLock() from
523  // BubbleManager::UpdateRevealedLock() because |widget| is not yet active
524  // at that time.
525  UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_YES);
526}
527
528////////////////////////////////////////////////////////////////////////////////
529// Animation delegate:
530
531void ImmersiveModeControllerAsh::AnimationEnded(
532    const ui::Animation* animation) {
533  if (reveal_state_ == SLIDING_OPEN) {
534    // AnimationProgressed() is called immediately before AnimationEnded()
535    // and does a layout.
536    OnSlideOpenAnimationCompleted(LAYOUT_NO);
537  } else if (reveal_state_ == SLIDING_CLOSED) {
538    OnSlideClosedAnimationCompleted();
539  }
540}
541
542void ImmersiveModeControllerAsh::AnimationProgressed(
543    const ui::Animation* animation) {
544  // Relayout. This will also move any views whose position depends on the
545  // top container position such as the find bar.
546  // We do not call LayoutBrowserRootView() here because we are not toggling
547  // the tab strip's immersive style so relaying out the non client view is not
548  // necessary.
549  top_container_->parent()->Layout();
550}
551
552////////////////////////////////////////////////////////////////////////////////
553// aura::WindowObserver overrides:
554void ImmersiveModeControllerAsh::OnWindowPropertyChanged(aura::Window* window,
555                                                         const void* key,
556                                                         intptr_t old) {
557  if (!enabled_)
558    return;
559
560  if (key == aura::client::kShowStateKey) {
561    // Disable immersive mode when the user exits fullscreen without going
562    // through FullscreenController::ToggleFullscreenMode(). This is the case
563    // if the user exits fullscreen via the restore button.
564    if (ShouldExitImmersiveFullscreen()) {
565      // Other "property change" observers may want to animate between the
566      // current visuals and the new window state. Do not alter the current
567      // visuals yet and post a task to exit immersive fullscreen instead.
568      base::MessageLoopForUI::current()->PostTask(
569          FROM_HERE,
570          base::Bind(&ImmersiveModeControllerAsh::MaybeExitImmersiveFullscreen,
571                     weak_ptr_factory_.GetWeakPtr()));
572    }
573
574    ui::WindowShowState show_state = native_window_->GetProperty(
575        aura::client::kShowStateKey);
576    if (show_state == ui::SHOW_STATE_FULLSCREEN &&
577        old == ui::SHOW_STATE_MINIMIZED) {
578      // Relayout in case there was a layout while the window show state was
579      // ui::SHOW_STATE_MINIMIZED.
580      LayoutBrowserRootView();
581    }
582  }
583}
584
585void ImmersiveModeControllerAsh::OnAddTransientChild(aura::Window* window,
586                                                     aura::Window* transient) {
587  views::BubbleDelegateView* bubble_delegate = AsBubbleDelegate(transient);
588  if (bubble_delegate &&
589      bubble_delegate->anchor_view() &&
590      top_container_->Contains(bubble_delegate->anchor_view())) {
591    // Observe the aura::Window because the BubbleDelegateView may not be
592    // parented to the widget's root view yet so |bubble_delegate->GetWidget()|
593    // may still return NULL.
594    bubble_manager_->StartObserving(transient);
595  }
596}
597
598void ImmersiveModeControllerAsh::OnRemoveTransientChild(
599    aura::Window* window,
600    aura::Window* transient) {
601  bubble_manager_->StopObserving(transient);
602}
603
604////////////////////////////////////////////////////////////////////////////////
605// Testing interface:
606
607void ImmersiveModeControllerAsh::SetForceHideTabIndicatorsForTest(bool force) {
608  if (force)
609    tab_indicator_visibility_ = TAB_INDICATORS_FORCE_HIDE;
610  else if (tab_indicator_visibility_ == TAB_INDICATORS_FORCE_HIDE)
611    tab_indicator_visibility_ = TAB_INDICATORS_HIDE;
612  UpdateUseMinimalChrome(LAYOUT_YES);
613}
614
615void ImmersiveModeControllerAsh::StartRevealForTest(bool hovered) {
616  MaybeStartReveal(ANIMATE_NO);
617  MoveMouse(top_container_, hovered);
618  UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_NO);
619}
620
621void ImmersiveModeControllerAsh::SetMouseHoveredForTest(bool hovered) {
622  MoveMouse(top_container_, hovered);
623  UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_NO);
624}
625
626void ImmersiveModeControllerAsh::DisableAnimationsForTest() {
627  animations_disabled_for_test_ = true;
628}
629
630////////////////////////////////////////////////////////////////////////////////
631// private:
632
633void ImmersiveModeControllerAsh::EnableWindowObservers(bool enable) {
634  if (observers_enabled_ == enable)
635    return;
636  observers_enabled_ = enable;
637
638  if (!native_window_) {
639    NOTREACHED() << "ImmersiveModeControllerAsh not initialized";
640    return;
641  }
642
643  views::Widget* widget =
644      views::Widget::GetWidgetForNativeWindow(native_window_);
645  views::FocusManager* focus_manager = widget->GetFocusManager();
646  if (enable) {
647    widget->AddObserver(this);
648    focus_manager->AddFocusChangeListener(this);
649  } else {
650    widget->RemoveObserver(this);
651    focus_manager->RemoveFocusChangeListener(this);
652  }
653
654  if (enable)
655    ash::Shell::GetInstance()->AddPreTargetHandler(this);
656  else
657    ash::Shell::GetInstance()->RemovePreTargetHandler(this);
658
659  if (enable) {
660    native_window_->AddObserver(this);
661  } else {
662    native_window_->RemoveObserver(this);
663  }
664
665  if (enable) {
666    RecreateBubbleManager();
667  } else {
668    // We have stopped observing whether transient children are added or removed
669    // to |native_window_|. The set of bubbles that BubbleManager is observing
670    // will become stale really quickly. Destroy BubbleManager and recreate it
671    // when we start observing |native_window_| again.
672    bubble_manager_.reset();
673  }
674
675  if (enable) {
676    registrar_.Add(
677        this,
678        chrome::NOTIFICATION_FULLSCREEN_CHANGED,
679        content::Source<FullscreenController>(
680            delegate_->GetFullscreenController()));
681  } else {
682    registrar_.Remove(
683        this,
684        chrome::NOTIFICATION_FULLSCREEN_CHANGED,
685        content::Source<FullscreenController>(
686            delegate_->GetFullscreenController()));
687  }
688
689  if (!enable)
690    animation_->Stop();
691}
692
693void ImmersiveModeControllerAsh::UpdateTopEdgeHoverTimer(
694    ui::MouseEvent* event) {
695  DCHECK(enabled_);
696  // Stop the timer if the top-of-window views are already revealed.
697  if (reveal_state_ == SLIDING_OPEN || reveal_state_ == REVEALED) {
698    top_edge_hover_timer_.Stop();
699    return;
700  }
701
702  gfx::Point location_in_screen = GetEventLocationInScreen(*event);
703  if (ShouldIgnoreMouseEventAtLocation(location_in_screen))
704    return;
705
706  // Stop the timer if the cursor left the top edge or is on a different
707  // display. The bounds of |top_container_|'s parent are used to infer the hit
708  // bounds because |top_container_| will be partially offscreen if it is
709  // animating closed.
710  gfx::Rect hit_bounds_in_screen =
711      top_container_->parent()->GetBoundsInScreen();
712  hit_bounds_in_screen.set_height(kMouseRevealBoundsHeight);
713  if (!hit_bounds_in_screen.Contains(location_in_screen)) {
714    top_edge_hover_timer_.Stop();
715    return;
716  }
717
718  // The cursor is now at the top of the screen. Consider the cursor "not
719  // moving" even if it moves a little bit because users don't have perfect
720  // pointing precision. (The y position is not tested because
721  // |hit_bounds_in_screen| is short.)
722  if (top_edge_hover_timer_.IsRunning() &&
723      abs(location_in_screen.x() - mouse_x_when_hit_top_in_screen_) <=
724          ImmersiveFullscreenConfiguration::
725              immersive_mode_reveal_x_threshold_pixels())
726    return;
727
728  // Start the reveal if the cursor doesn't move for some amount of time.
729  mouse_x_when_hit_top_in_screen_ = location_in_screen.x();
730  top_edge_hover_timer_.Stop();
731  // Timer is stopped when |this| is destroyed, hence Unretained() is safe.
732  top_edge_hover_timer_.Start(
733      FROM_HERE,
734      base::TimeDelta::FromMilliseconds(
735          ImmersiveFullscreenConfiguration::immersive_mode_reveal_delay_ms()),
736      base::Bind(&ImmersiveModeControllerAsh::AcquireLocatedEventRevealedLock,
737                 base::Unretained(this)));
738}
739
740void ImmersiveModeControllerAsh::UpdateLocatedEventRevealedLock(
741    ui::LocatedEvent* event,
742    AllowRevealWhileClosing allow_reveal_while_closing) {
743  if (!enabled_)
744    return;
745  DCHECK(!event || event->IsMouseEvent() || event->IsTouchEvent());
746
747  // Neither the mouse nor touch can initiate a reveal when the top-of-window
748  // views are sliding closed or are closed with the following exceptions:
749  // - Hovering at y = 0 which is handled in OnMouseEvent().
750  // - Doing a SWIPE_OPEN edge gesture which is handled in OnGestureEvent().
751  if (reveal_state_ == CLOSED ||
752      (reveal_state_ == SLIDING_CLOSED &&
753       allow_reveal_while_closing == ALLOW_REVEAL_WHILE_CLOSING_NO)) {
754    return;
755  }
756
757  // Neither the mouse nor touch should keep the top-of-window views revealed if
758  // |native_window_| is not active.
759  if (!views::Widget::GetWidgetForNativeWindow(native_window_)->IsActive()) {
760    located_event_revealed_lock_.reset();
761    return;
762  }
763
764  // Ignore all events while a window has capture. This keeps the top-of-window
765  // views revealed during a drag.
766  if (aura::client::GetCaptureWindow(native_window_))
767    return;
768
769  gfx::Point location_in_screen;
770  if (event && event->type() != ui::ET_MOUSE_CAPTURE_CHANGED) {
771    location_in_screen = GetEventLocationInScreen(*event);
772  } else {
773    aura::client::CursorClient* cursor_client = aura::client::GetCursorClient(
774        native_window_->GetRootWindow());
775    if (!cursor_client->IsMouseEventsEnabled()) {
776      // If mouse events are disabled, the user's last interaction was probably
777      // via touch. Do no do further processing in this case as there is no easy
778      // way of retrieving the position of the user's last touch.
779      return;
780    }
781    location_in_screen = aura::Env::GetInstance()->last_mouse_location();
782  }
783
784  if ((!event || event->IsMouseEvent()) &&
785      ShouldIgnoreMouseEventAtLocation(location_in_screen)) {
786    return;
787  }
788
789  gfx::Rect hit_bounds_in_top_container = top_container_->GetVisibleBounds();
790  // TODO(tdanderson): Implement View::ConvertRectToScreen();
791  gfx::Point hit_bounds_in_screen_origin = hit_bounds_in_top_container.origin();
792  views::View::ConvertPointToScreen(top_container_,
793      &hit_bounds_in_screen_origin);
794  gfx::Rect hit_bounds_in_screen(hit_bounds_in_screen_origin,
795      hit_bounds_in_top_container.size());
796
797  gfx::Rect find_bar_hit_bounds_in_screen = find_bar_visible_bounds_in_screen_;
798
799  // Allow the cursor to move slightly off the top-of-window views before
800  // sliding closed. This helps when the user is attempting to click on the
801  // bookmark bar and overshoots slightly.
802  if (event && event->type() == ui::ET_MOUSE_MOVED) {
803    const int kBoundsOffsetY = 8;
804    hit_bounds_in_screen.Inset(0, 0, 0, -kBoundsOffsetY);
805    find_bar_hit_bounds_in_screen.Inset(0, 0, 0, -kBoundsOffsetY);
806  }
807
808  if (hit_bounds_in_screen.Contains(location_in_screen) ||
809      find_bar_hit_bounds_in_screen.Contains(location_in_screen)) {
810    AcquireLocatedEventRevealedLock();
811  } else {
812    located_event_revealed_lock_.reset();
813  }
814}
815
816void ImmersiveModeControllerAsh::AcquireLocatedEventRevealedLock() {
817  // CAUTION: Acquiring the lock results in a reentrant call to
818  // AcquireLocatedEventRevealedLock() when
819  // |ImmersiveModeControllerAsh::animations_disabled_for_test_| is true.
820  if (!located_event_revealed_lock_.get())
821    located_event_revealed_lock_.reset(GetRevealedLock(ANIMATE_REVEAL_YES));
822}
823
824void ImmersiveModeControllerAsh::UpdateFocusRevealedLock() {
825  if (!enabled_)
826    return;
827
828  bool hold_lock = false;
829  views::Widget* widget =
830      views::Widget::GetWidgetForNativeWindow(native_window_);
831  if (widget->IsActive()) {
832    views::View* focused_view = widget->GetFocusManager()->GetFocusedView();
833    if (top_container_->Contains(focused_view))
834      hold_lock = true;
835  } else {
836    aura::Window* active_window = aura::client::GetActivationClient(
837        native_window_->GetRootWindow())->GetActiveWindow();
838    views::BubbleDelegateView* bubble_delegate =
839        AsBubbleDelegate(active_window);
840    if (bubble_delegate && bubble_delegate->anchor_view()) {
841      // BubbleManager will already have locked the top-of-window views if the
842      // bubble is anchored to a child of |top_container_|. Don't acquire
843      // |focus_revealed_lock_| here for the sake of simplicity.
844    } else {
845      // The currently active window is not |native_window_| and it is not a
846      // bubble with an anchor view. The top-of-window views should be revealed
847      // if:
848      // 1) The active window is a transient child of |native_window_|.
849      // 2) The top-of-window views are already revealed. This restriction
850      //    prevents a transient window opened by the web contents while the
851      //    top-of-window views are hidden from from initiating a reveal.
852      // The top-of-window views will stay revealed till |native_window_| is
853      // reactivated.
854      if (IsRevealed() &&
855          IsWindowTransientChildOf(active_window, native_window_)) {
856        hold_lock = true;
857      }
858    }
859  }
860
861  if (hold_lock) {
862    if (!focus_revealed_lock_.get())
863      focus_revealed_lock_.reset(GetRevealedLock(ANIMATE_REVEAL_YES));
864  } else {
865    focus_revealed_lock_.reset();
866  }
867}
868
869bool ImmersiveModeControllerAsh::UpdateRevealedLocksForSwipe(
870    SwipeType swipe_type) {
871  if (!enabled_ || swipe_type == SWIPE_NONE)
872    return false;
873
874  // Swipes while |native_window_| is inactive should have been filtered out in
875  // OnGestureEvent().
876  DCHECK(views::Widget::GetWidgetForNativeWindow(native_window_)->IsActive());
877
878  if (reveal_state_ == SLIDING_CLOSED || reveal_state_ == CLOSED) {
879    if (swipe_type == SWIPE_OPEN && !located_event_revealed_lock_.get()) {
880      located_event_revealed_lock_.reset(GetRevealedLock(ANIMATE_REVEAL_YES));
881      return true;
882    }
883  } else {
884    if (swipe_type == SWIPE_CLOSE) {
885      // Attempt to end the reveal. If other code is holding onto a lock, the
886      // attempt will be unsuccessful.
887      located_event_revealed_lock_.reset();
888      focus_revealed_lock_.reset();
889
890      if (reveal_state_ == SLIDING_CLOSED || reveal_state_ == CLOSED)
891        return true;
892
893      // Ending the reveal was unsuccessful. Reaquire the locks if appropriate.
894      UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_NO);
895      UpdateFocusRevealedLock();
896    }
897  }
898  return false;
899}
900
901void ImmersiveModeControllerAsh::UpdateUseMinimalChrome(Layout layout) {
902  // May be NULL in tests.
903  FullscreenController* fullscreen_controller =
904      delegate_->GetFullscreenController();
905  bool in_tab_fullscreen = fullscreen_controller ?
906      fullscreen_controller->IsFullscreenForTabOrPending() : false;
907  bool use_minimal_chrome = !in_tab_fullscreen && enabled_;
908  native_window_->SetProperty(ash::internal::kFullscreenUsesMinimalChromeKey,
909                              use_minimal_chrome);
910
911  TabIndicatorVisibility previous_tab_indicator_visibility =
912      tab_indicator_visibility_;
913  if (tab_indicator_visibility_ != TAB_INDICATORS_FORCE_HIDE) {
914    tab_indicator_visibility_ = use_minimal_chrome ?
915        TAB_INDICATORS_SHOW : TAB_INDICATORS_HIDE;
916  }
917
918  // Ash on Windows may not have a shell.
919  if (ash::Shell::HasInstance()) {
920    // When using minimal chrome, the shelf is auto-hidden. The auto-hidden
921    // shelf displays a 3px 'light bar' when it is closed.
922    ash::Shell::GetInstance()->UpdateShelfVisibility();
923  }
924
925  if (tab_indicator_visibility_ != previous_tab_indicator_visibility) {
926    // If the top-of-window views are revealed or animating, the change will
927    // take effect with the layout once the top-of-window views are closed.
928    if (layout == LAYOUT_YES && reveal_state_ == CLOSED)
929      LayoutBrowserRootView();
930  }
931}
932
933int ImmersiveModeControllerAsh::GetAnimationDuration(Animate animate) const {
934  switch (animate) {
935    case ANIMATE_NO:
936      return 0;
937    case ANIMATE_SLOW:
938      return kRevealSlowAnimationDurationMs;
939    case ANIMATE_FAST:
940      return kRevealFastAnimationDurationMs;
941  }
942  NOTREACHED();
943  return 0;
944}
945
946void ImmersiveModeControllerAsh::MaybeStartReveal(Animate animate) {
947  if (!enabled_)
948    return;
949
950  if (animations_disabled_for_test_)
951    animate = ANIMATE_NO;
952
953  // Callers with ANIMATE_NO expect this function to synchronously reveal the
954  // top-of-window views.
955  if (reveal_state_ == REVEALED ||
956      (reveal_state_ == SLIDING_OPEN && animate != ANIMATE_NO)) {
957    return;
958  }
959
960  RevealState previous_reveal_state = reveal_state_;
961  reveal_state_ = SLIDING_OPEN;
962  if (previous_reveal_state == CLOSED) {
963    // Turn on layer painting so that we can overlap the web contents.
964    EnablePaintToLayer(true);
965
966    // Ensure window caption buttons are updated and the view bounds are
967    // computed at normal (non-immersive-style) size. The layout call moves the
968    // top-of-window views to their initial offscreen position for the
969    // animation.
970    delegate_->SetImmersiveStyle(false);
971    SetRenderWindowTopInsetsForTouch(0);
972    LayoutBrowserRootView();
973
974    // Do not do any more processing if LayoutBrowserView() changed
975    // |reveal_state_|.
976    if (reveal_state_ != SLIDING_OPEN) {
977      if (reveal_state_ == REVEALED)
978        FOR_EACH_OBSERVER(Observer, observers_, OnImmersiveRevealStarted());
979      return;
980    }
981  }
982  // Slide in the reveal view.
983  if (animate == ANIMATE_NO) {
984    animation_->Reset(1);
985    OnSlideOpenAnimationCompleted(LAYOUT_YES);
986  } else {
987    animation_->SetSlideDuration(GetAnimationDuration(animate));
988    animation_->Show();
989  }
990
991  if (previous_reveal_state == CLOSED)
992     FOR_EACH_OBSERVER(Observer, observers_, OnImmersiveRevealStarted());
993}
994
995void ImmersiveModeControllerAsh::EnablePaintToLayer(bool enable) {
996  top_container_->SetPaintToLayer(enable);
997
998  // Views software compositing is not fully layer aware. If the bookmark bar
999  // is detached while the top container layer slides on or off the screen,
1000  // the pixels that become exposed are the remnants of the last software
1001  // composite of the BrowserView, not the freshly-exposed bookmark bar.
1002  // Force the bookmark bar to paint to a layer so the views composite
1003  // properly. The infobar container does not need this treatment because
1004  // BrowserView::PaintChildren() always draws it last when it is visible.
1005  BookmarkBarView* bookmark_bar = delegate_->GetBookmarkBar();
1006  if (!bookmark_bar)
1007    return;
1008  if (enable && bookmark_bar->IsDetached())
1009    bookmark_bar->SetPaintToLayer(true);
1010  else
1011    bookmark_bar->SetPaintToLayer(false);
1012}
1013
1014void ImmersiveModeControllerAsh::LayoutBrowserRootView() {
1015  // Update the window caption buttons.
1016  widget_->non_client_view()->frame_view()->ResetWindowControls();
1017  // Layout all views, including BrowserView.
1018  widget_->GetRootView()->Layout();
1019}
1020
1021void ImmersiveModeControllerAsh::OnSlideOpenAnimationCompleted(Layout layout) {
1022  DCHECK_EQ(SLIDING_OPEN, reveal_state_);
1023  reveal_state_ = REVEALED;
1024
1025  if (layout == LAYOUT_YES)
1026    top_container_->parent()->Layout();
1027
1028  // The user may not have moved the mouse since the reveal was initiated.
1029  // Update the revealed lock to reflect the mouse's current state.
1030  UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_NO);
1031}
1032
1033void ImmersiveModeControllerAsh::MaybeEndReveal(Animate animate) {
1034  if (!enabled_ || revealed_lock_count_ != 0)
1035    return;
1036
1037  if (animations_disabled_for_test_)
1038    animate = ANIMATE_NO;
1039
1040  // Callers with ANIMATE_NO expect this function to synchronously close the
1041  // top-of-window views.
1042  if (reveal_state_ == CLOSED ||
1043      (reveal_state_ == SLIDING_CLOSED && animate != ANIMATE_NO)) {
1044    return;
1045  }
1046
1047  reveal_state_ = SLIDING_CLOSED;
1048  int duration_ms = GetAnimationDuration(animate);
1049  if (duration_ms > 0) {
1050    // The bookmark bar may have become detached during the reveal so ensure
1051    // layers are available. This is a no-op for the top container.
1052    EnablePaintToLayer(true);
1053
1054    animation_->SetSlideDuration(duration_ms);
1055    animation_->Hide();
1056  } else {
1057    animation_->Reset(0);
1058    OnSlideClosedAnimationCompleted();
1059  }
1060}
1061
1062void ImmersiveModeControllerAsh::OnSlideClosedAnimationCompleted() {
1063  DCHECK_EQ(SLIDING_CLOSED, reveal_state_);
1064  reveal_state_ = CLOSED;
1065  // Layers aren't needed after animation completes.
1066  EnablePaintToLayer(false);
1067  // Update tabstrip for closed state.
1068  delegate_->SetImmersiveStyle(true);
1069  SetRenderWindowTopInsetsForTouch(kNearTopContainerDistance);
1070  LayoutBrowserRootView();
1071}
1072
1073bool ImmersiveModeControllerAsh::ShouldExitImmersiveFullscreen() const {
1074  if (!native_window_)
1075    return false;
1076
1077  ui::WindowShowState show_state = static_cast<ui::WindowShowState>(
1078      native_window_->GetProperty(aura::client::kShowStateKey));
1079  return IsEnabled() &&
1080      show_state != ui::SHOW_STATE_FULLSCREEN &&
1081      show_state != ui::SHOW_STATE_MINIMIZED;
1082}
1083
1084ImmersiveModeControllerAsh::SwipeType ImmersiveModeControllerAsh::GetSwipeType(
1085    ui::GestureEvent* event) const {
1086  if (event->type() != ui::ET_GESTURE_SCROLL_UPDATE)
1087    return SWIPE_NONE;
1088  // Make sure that it is a clear vertical gesture.
1089  if (abs(event->details().scroll_y()) <=
1090      kSwipeVerticalThresholdMultiplier * abs(event->details().scroll_x()))
1091    return SWIPE_NONE;
1092  if (event->details().scroll_y() < 0)
1093    return SWIPE_CLOSE;
1094  else if (event->details().scroll_y() > 0)
1095    return SWIPE_OPEN;
1096  return SWIPE_NONE;
1097}
1098
1099bool ImmersiveModeControllerAsh::ShouldIgnoreMouseEventAtLocation(
1100    const gfx::Point& location) const {
1101  // Ignore mouse events in the region immediately above the top edge of the
1102  // display. This is to handle the case of a user with a vertical display
1103  // layout (primary display above/below secondary display) and the immersive
1104  // fullscreen window on the bottom display. It is really hard to trigger a
1105  // reveal in this case because:
1106  // - It is hard to stop the cursor in the top |kMouseRevealBoundsHeight|
1107  //   pixels of the bottom display.
1108  // - The cursor is warped to the top display if the cursor gets to the top
1109  //   edge of the bottom display.
1110  // Mouse events are ignored in the bottom few pixels of the top display
1111  // (Mouse events in this region cannot start or end a reveal). This allows a
1112  // user to overshoot the top of the bottom display and still reveal the
1113  // top-of-window views.
1114  gfx::Rect dead_region = top_container_->parent()->GetBoundsInScreen();
1115  dead_region.set_y(dead_region.y() - kHeightOfDeadRegionAboveTopContainer);
1116  dead_region.set_height(kHeightOfDeadRegionAboveTopContainer);
1117  return dead_region.Contains(location);
1118}
1119
1120bool ImmersiveModeControllerAsh::ShouldHandleGestureEvent(
1121    const gfx::Point& location) const {
1122  gfx::Rect top_container_bounds_in_screen =
1123      top_container_->GetBoundsInScreen();
1124
1125  // All of the gestures that are of interest start in a region with left &
1126  // right edges agreeing with |top_container_|. When CLOSED it is difficult to
1127  // hit the bounds due to small size of the tab strip, so the hit target needs
1128  // to be extended on the bottom, thus the inset call.
1129  gfx::Rect near_bounds = top_container_bounds_in_screen;
1130  if (reveal_state_ == CLOSED)
1131    near_bounds.Inset(gfx::Insets(0, 0, -kNearTopContainerDistance, 0));
1132  if (near_bounds.Contains(location))
1133    return true;
1134
1135  // There may be a bezel sensor off screen logically above |top_container_|
1136  // thus the test needs to include gestures starting above, but this needs to
1137  // be distinguished from events originating on another screen from
1138  // (potentially) an extended desktop. The check for the event not contained by
1139  // the closest screen ensures that the event is from a valid bezel and can be
1140  // interpreted as such.
1141  gfx::Rect screen_bounds =
1142      ash::Shell::GetScreen()->GetDisplayNearestPoint(location).bounds();
1143  return (!screen_bounds.Contains(location) &&
1144          location.y() < top_container_bounds_in_screen.y() &&
1145          location.x() >= top_container_bounds_in_screen.x() &&
1146          location.x() < top_container_bounds_in_screen.right());
1147}
1148
1149void ImmersiveModeControllerAsh::SetRenderWindowTopInsetsForTouch(
1150    int top_inset) {
1151  content::WebContents* contents = delegate_->GetWebContents();
1152  if (contents) {
1153    aura::Window* window = contents->GetView()->GetContentNativeView();
1154    // |window| is NULL if the renderer crashed.
1155    if (window) {
1156      gfx::Insets inset(top_inset, 0, 0, 0);
1157      window->SetHitTestBoundsOverrideOuter(
1158          window->hit_test_bounds_override_outer_mouse(),
1159          inset);
1160    }
1161  }
1162}
1163
1164void ImmersiveModeControllerAsh::RecreateBubbleManager() {
1165  bubble_manager_.reset(new BubbleManager(this));
1166  const std::vector<aura::Window*> transient_children =
1167      native_window_->transient_children();
1168  for (size_t i = 0; i < transient_children.size(); ++i) {
1169    aura::Window* transient_child = transient_children[i];
1170    views::BubbleDelegateView* bubble_delegate =
1171        AsBubbleDelegate(transient_child);
1172    if (bubble_delegate &&
1173        bubble_delegate->anchor_view() &&
1174        top_container_->Contains(bubble_delegate->anchor_view())) {
1175      bubble_manager_->StartObserving(transient_child);
1176    }
1177  }
1178}
1179