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 36 using views::View; 37 38 namespace { 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. 43 const 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. 48 const int kRevealSlowAnimationDurationMs = 400; 49 const 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. 54 const 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. 59 const 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. 65 const 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). 72 const 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. 77 void 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. 89 views::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|. 99 bool 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. 113 gfx::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 124 class 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. 156 class 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 185 ImmersiveModeControllerAsh::BubbleManager::BubbleManager( 186 ImmersiveModeControllerAsh* controller) 187 : controller_(controller) { 188 } 189 190 ImmersiveModeControllerAsh::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 197 void ImmersiveModeControllerAsh::BubbleManager::StartObserving( 198 aura::Window* bubble) { 199 if (bubbles_.insert(bubble).second) { 200 bubble->AddObserver(this); 201 UpdateRevealedLock(); 202 } 203 } 204 205 void ImmersiveModeControllerAsh::BubbleManager::StopObserving( 206 aura::Window* bubble) { 207 if (bubbles_.erase(bubble)) { 208 bubble->RemoveObserver(this); 209 UpdateRevealedLock(); 210 } 211 } 212 213 void 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 249 void ImmersiveModeControllerAsh::BubbleManager::OnWindowVisibilityChanged( 250 aura::Window*, 251 bool visible) { 252 UpdateRevealedLock(); 253 } 254 255 void ImmersiveModeControllerAsh::BubbleManager::OnWindowDestroying( 256 aura::Window* window) { 257 StopObserving(window); 258 } 259 260 //////////////////////////////////////////////////////////////////////////////// 261 262 ImmersiveModeControllerAsh::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 279 ImmersiveModeControllerAsh::~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 286 void 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 294 void 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 303 void ImmersiveModeControllerAsh::MaybeExitImmersiveFullscreen() { 304 if (ShouldExitImmersiveFullscreen()) 305 delegate_->FullscreenStateChanged(); 306 } 307 308 void 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 326 void 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 374 bool ImmersiveModeControllerAsh::IsEnabled() const { 375 return enabled_; 376 } 377 378 bool ImmersiveModeControllerAsh::ShouldHideTabIndicators() const { 379 return tab_indicator_visibility_ != TAB_INDICATORS_SHOW; 380 } 381 382 bool ImmersiveModeControllerAsh::ShouldHideTopViews() const { 383 return enabled_ && reveal_state_ == CLOSED; 384 } 385 386 bool ImmersiveModeControllerAsh::IsRevealed() const { 387 return enabled_ && reveal_state_ != CLOSED; 388 } 389 390 int 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 399 ImmersiveRevealedLock* ImmersiveModeControllerAsh::GetRevealedLock( 400 AnimateReveal animate_reveal) { 401 return new RevealedLockAsh(weak_ptr_factory_.GetWeakPtr(), animate_reveal); 402 } 403 404 void 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 412 void 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 421 void 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 451 void 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 458 void 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 490 void ImmersiveModeControllerAsh::OnWillChangeFocus(views::View* focused_before, 491 views::View* focused_now) { 492 } 493 494 void ImmersiveModeControllerAsh::OnDidChangeFocus(views::View* focused_before, 495 views::View* focused_now) { 496 UpdateFocusRevealedLock(); 497 } 498 499 void 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 508 void 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 531 void 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 542 void 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: 554 void 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 585 void 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 598 void ImmersiveModeControllerAsh::OnRemoveTransientChild( 599 aura::Window* window, 600 aura::Window* transient) { 601 bubble_manager_->StopObserving(transient); 602 } 603 604 //////////////////////////////////////////////////////////////////////////////// 605 // Testing interface: 606 607 void 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 615 void ImmersiveModeControllerAsh::StartRevealForTest(bool hovered) { 616 MaybeStartReveal(ANIMATE_NO); 617 MoveMouse(top_container_, hovered); 618 UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_NO); 619 } 620 621 void ImmersiveModeControllerAsh::SetMouseHoveredForTest(bool hovered) { 622 MoveMouse(top_container_, hovered); 623 UpdateLocatedEventRevealedLock(NULL, ALLOW_REVEAL_WHILE_CLOSING_NO); 624 } 625 626 void ImmersiveModeControllerAsh::DisableAnimationsForTest() { 627 animations_disabled_for_test_ = true; 628 } 629 630 //////////////////////////////////////////////////////////////////////////////// 631 // private: 632 633 void 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 693 void 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 740 void 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 816 void 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 824 void 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 869 bool 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 901 void 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 933 int 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 946 void 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 995 void 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 1014 void 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 1021 void 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 1033 void 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 1062 void 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 1073 bool 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 1084 ImmersiveModeControllerAsh::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 1099 bool 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 1120 bool 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 1149 void 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 1164 void 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