Home | History | Annotate | Download | only in wm
      1 // Copyright 2014 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 "athena/wm/split_view_controller.h"
      6 
      7 #include <cmath>
      8 
      9 #include "athena/screen/public/screen_manager.h"
     10 #include "athena/wm/public/window_list_provider.h"
     11 #include "athena/wm/public/window_manager.h"
     12 #include "base/bind.h"
     13 #include "ui/aura/scoped_window_targeter.h"
     14 #include "ui/aura/window.h"
     15 #include "ui/aura/window_targeter.h"
     16 #include "ui/compositor/closure_animation_observer.h"
     17 #include "ui/compositor/layer.h"
     18 #include "ui/compositor/scoped_layer_animation_settings.h"
     19 #include "ui/events/event_handler.h"
     20 #include "ui/gfx/display.h"
     21 #include "ui/gfx/screen.h"
     22 #include "ui/views/background.h"
     23 #include "ui/views/layout/box_layout.h"
     24 #include "ui/views/widget/root_view.h"
     25 #include "ui/views/widget/root_view_targeter.h"
     26 #include "ui/views/widget/widget.h"
     27 #include "ui/wm/core/window_util.h"
     28 #include "ui/wm/public/activation_client.h"
     29 
     30 namespace athena {
     31 
     32 namespace {
     33 
     34 const int kDragHandleWidth = 4;
     35 const int kDragHandleHeight = 80;
     36 const int kDragHandleMargin = 1;
     37 const int kDividerWidth = kDragHandleWidth + 2 * kDragHandleMargin;
     38 
     39 // Always returns the same target.
     40 class StaticViewTargeterDelegate : public views::ViewTargeterDelegate {
     41  public:
     42   explicit StaticViewTargeterDelegate(views::View* target) : target_(target) {}
     43 
     44   virtual ~StaticViewTargeterDelegate() {}
     45 
     46  private:
     47   // views::ViewTargeterDelegate:
     48   virtual views::View* TargetForRect(views::View* root,
     49                                      const gfx::Rect& rect) OVERRIDE {
     50     return target_;
     51   }
     52 
     53   // Not owned.
     54   views::View* target_;
     55 
     56   DISALLOW_COPY_AND_ASSIGN(StaticViewTargeterDelegate);
     57 };
     58 
     59 // Expands the effective target area of the window of the widget containing the
     60 // specified view. If the view is large enough to begin with, there should be
     61 // no change from the default targeting behavior.
     62 class PriorityWindowTargeter : public aura::WindowTargeter,
     63                                public aura::WindowObserver {
     64  public:
     65   explicit PriorityWindowTargeter(views::View* priority_view)
     66       : priority_view_(priority_view) {
     67     CHECK(priority_view->GetWidget());
     68     window_ = priority_view->GetWidget()->GetNativeWindow();
     69     CHECK(window_);
     70     window_->AddObserver(this);
     71   }
     72 
     73   virtual ~PriorityWindowTargeter() {
     74     window_->RemoveObserver(this);
     75   }
     76 
     77  private:
     78   // aura::WindowTargeter:
     79   virtual ui::EventTarget* FindTargetForLocatedEvent(
     80       ui::EventTarget* root,
     81       ui::LocatedEvent* event) OVERRIDE {
     82     if (!window_ || (event->type() != ui::ET_TOUCH_PRESSED))
     83       return WindowTargeter::FindTargetForLocatedEvent(root, event);
     84     CHECK_EQ(window_, priority_view_->GetWidget()->GetNativeWindow());
     85 
     86     // Bounds of the view in root window's coordinates.
     87     gfx::Rect view_bounds = priority_view_->GetBoundsInScreen();
     88     // If there is a transform on the window's layer - apply it.
     89     gfx::Transform window_transform = window_->layer()->transform();
     90     gfx::RectF transformed_bounds_f = view_bounds;
     91     window_transform.TransformRect(&transformed_bounds_f);
     92     gfx::Rect transformed_bounds = gfx::Rect(transformed_bounds_f.x(),
     93                                              transformed_bounds_f.y(),
     94                                              transformed_bounds_f.width(),
     95                                              transformed_bounds_f.height());
     96     // Now expand the bounds to be at least
     97     // kMinTouchDimension x kMinTouchDimension and target the event to the
     98     // window if it falls within the expanded bounds
     99     gfx::Point center = transformed_bounds.CenterPoint();
    100     gfx::Rect extension_rect = gfx::Rect(
    101         center.x() - kMinTouchDimension / 2,
    102         center.y() - kMinTouchDimension / 2,
    103         kMinTouchDimension,
    104         kMinTouchDimension);
    105     gfx::Rect extended_bounds =
    106         gfx::UnionRects(transformed_bounds, extension_rect);
    107     if (extended_bounds.Contains(event->root_location())) {
    108       root->ConvertEventToTarget(window_, event);
    109       return window_;
    110     }
    111 
    112     return WindowTargeter::FindTargetForLocatedEvent(root, event);
    113   }
    114 
    115   // aura::WindowObserver:
    116   virtual void OnWindowDestroying(aura::Window* window) OVERRIDE {
    117     DCHECK_EQ(window, window_);
    118     window_->RemoveObserver(this);
    119     window_ = NULL;
    120   }
    121 
    122   // Minimum dimension of a target to be comfortably touchable.
    123   // The effective touch target area of |priority_window_| gets expanded so
    124   // that it's width and height is ayt least |kMinTouchDimension|.
    125   int const kMinTouchDimension = 26;
    126 
    127   aura::Window* window_;
    128   views::View* priority_view_;
    129 
    130   DISALLOW_COPY_AND_ASSIGN(PriorityWindowTargeter);
    131 };
    132 
    133 // Returns a target transform required to transform |from| to |to|.
    134 gfx::Transform GetTransformForBounds(const gfx::Rect& from,
    135                                      const gfx::Rect& to) {
    136   gfx::Transform transform;
    137   transform.Translate(to.x() - from.x(), to.y() - from.y());
    138   transform.Scale(to.width() / static_cast<float>(from.width()),
    139                   to.height() / static_cast<float>(from.height()));
    140   return transform;
    141 }
    142 
    143 bool IsLandscapeOrientation(gfx::Display::Rotation rotation) {
    144   return rotation == gfx::Display::ROTATE_0 ||
    145          rotation == gfx::Display::ROTATE_180;
    146 }
    147 
    148 }  // namespace
    149 
    150 SplitViewController::SplitViewController(
    151     aura::Window* container,
    152     WindowListProvider* window_list_provider)
    153     : state_(INACTIVE),
    154       container_(container),
    155       window_list_provider_(window_list_provider),
    156       left_window_(NULL),
    157       right_window_(NULL),
    158       divider_position_(0),
    159       divider_scroll_start_position_(0),
    160       divider_widget_(NULL),
    161       drag_handle_(NULL),
    162       weak_factory_(this) {
    163 }
    164 
    165 SplitViewController::~SplitViewController() {
    166 }
    167 
    168 bool SplitViewController::CanActivateSplitViewMode() const {
    169   // TODO(mfomitchev): return false in full screen.
    170   return (!IsSplitViewModeActive() &&
    171               window_list_provider_->GetWindowList().size() >= 2 &&
    172               IsLandscapeOrientation(gfx::Screen::GetNativeScreen()->
    173                   GetDisplayNearestWindow(container_).rotation()));
    174 }
    175 
    176 bool SplitViewController::IsSplitViewModeActive() const {
    177   return state_ == ACTIVE;
    178 }
    179 
    180 void SplitViewController::ActivateSplitMode(aura::Window* left,
    181                                             aura::Window* right,
    182                                             aura::Window* to_activate) {
    183   const aura::Window::Windows& windows = window_list_provider_->GetWindowList();
    184   aura::Window::Windows::const_reverse_iterator iter = windows.rbegin();
    185   if (state_ == ACTIVE) {
    186     if (!left && left_window_ != right)
    187       left = left_window_;
    188     if (!right && right_window_ != left)
    189       right = right_window_;
    190   }
    191 
    192   if (!left && iter != windows.rend()) {
    193     left = *iter;
    194     iter++;
    195     if (left == right && iter != windows.rend()) {
    196       left = *iter;
    197       iter++;
    198     }
    199   }
    200 
    201   if (!right && iter != windows.rend()) {
    202     right = *iter;
    203     iter++;
    204     if (right == left && iter != windows.rend()) {
    205       right = *iter;
    206       iter++;
    207     }
    208   }
    209 
    210   to_hide_.clear();
    211   if (left_window_ && left_window_ != left && left_window_ != right)
    212     to_hide_.push_back(left_window_);
    213   if (right_window_ && right_window_ != left && right_window_ != right)
    214     to_hide_.push_back(right_window_);
    215 
    216   left_window_ = left;
    217   right_window_ = right;
    218 
    219   divider_position_ = GetDefaultDividerPosition();
    220   SetState(ACTIVE);
    221   UpdateLayout(true);
    222 
    223   aura::client::ActivationClient* activation_client =
    224       aura::client::GetActivationClient(container_->GetRootWindow());
    225   aura::Window* active_window = activation_client->GetActiveWindow();
    226   if (to_activate) {
    227     CHECK(to_activate == left_window_ || to_activate == right_window_);
    228     wm::ActivateWindow(to_activate);
    229   } else if (active_window != left_window_ &&
    230              active_window != right_window_) {
    231     // A window which does not belong to an activity could be active.
    232     wm::ActivateWindow(left_window_);
    233   }
    234   active_window = activation_client->GetActiveWindow();
    235 
    236   if (active_window == left_window_)
    237     window_list_provider_->StackWindowBehindTo(right_window_, left_window_);
    238   else
    239     window_list_provider_->StackWindowBehindTo(left_window_, right_window_);
    240 }
    241 
    242 void SplitViewController::ReplaceWindow(aura::Window* window,
    243                                         aura::Window* replace_with) {
    244   CHECK(IsSplitViewModeActive());
    245   CHECK(replace_with);
    246   CHECK(window == left_window_ || window == right_window_);
    247   CHECK(replace_with != left_window_ && replace_with != right_window_);
    248   DCHECK(window_list_provider_->IsWindowInList(replace_with));
    249 
    250   aura::Window* not_replaced = NULL;
    251   if (window == left_window_) {
    252     left_window_ = replace_with;
    253     not_replaced = right_window_;
    254   } else {
    255     right_window_ = replace_with;
    256     not_replaced = left_window_;
    257   }
    258   UpdateLayout(false);
    259 
    260   wm::ActivateWindow(replace_with);
    261   window_list_provider_->StackWindowBehindTo(not_replaced, replace_with);
    262 
    263   window->SetTransform(gfx::Transform());
    264   window->Hide();
    265 }
    266 
    267 void SplitViewController::DeactivateSplitMode() {
    268   CHECK_EQ(ACTIVE, state_);
    269   SetState(INACTIVE);
    270   UpdateLayout(false);
    271   left_window_ = right_window_ = NULL;
    272 }
    273 
    274 void SplitViewController::InitializeDivider() {
    275   CHECK(!divider_widget_);
    276   CHECK(!drag_handle_);
    277 
    278   drag_handle_ = CreateDragHandleView(DRAG_HANDLE_HORIZONTAL,
    279                                       this,
    280                                       kDragHandleWidth,
    281                                       kDragHandleHeight);
    282   views::View* content_view = new views::View;
    283   content_view->set_background(
    284       views::Background::CreateSolidBackground(SK_ColorBLACK));
    285   views::BoxLayout* layout =
    286       new views::BoxLayout(views::BoxLayout::kHorizontal,
    287                            kDragHandleMargin,
    288                            kDragHandleMargin,
    289                            0);
    290   layout->set_main_axis_alignment(views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER);
    291   layout->set_cross_axis_alignment(
    292       views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER);
    293   content_view->SetLayoutManager(layout);
    294   content_view->AddChildView(drag_handle_);
    295 
    296   divider_widget_ = new views::Widget();
    297   views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
    298   params.parent = container_;
    299   params.bounds = gfx::Rect(-kDividerWidth / 2,
    300                             0,
    301                             kDividerWidth,
    302                             container_->bounds().height());
    303   divider_widget_->Init(params);
    304   divider_widget_->SetContentsView(content_view);
    305 
    306   // Install a static view targeter on the root view which always targets
    307   // divider_view.
    308   // TODO(mfomitchev,tdanderson): This should not be needed:
    309   // 1. crbug.com/414339 - divider_view is the only view and it completely
    310   //    overlaps the root view.
    311   // 2. The logic in ViewTargeterDelegate::TargetForRect could be improved to
    312   //    work better for views that are narrow in one dimension and long in
    313   //    another dimension.
    314   views::internal::RootView* root_view =
    315       static_cast<views::internal::RootView*>(divider_widget_->GetRootView());
    316   view_targeter_delegate_.reset(new StaticViewTargeterDelegate(drag_handle_));
    317   views::ViewTargeter* targeter =
    318       new views::RootViewTargeter(view_targeter_delegate_.get(), root_view);
    319   divider_widget_->GetRootView()->SetEventTargeter(
    320       scoped_ptr<views::ViewTargeter>(targeter));
    321 }
    322 
    323 void SplitViewController::HideDivider() {
    324   divider_widget_->Hide();
    325   window_targeter_.reset();
    326 }
    327 
    328 void SplitViewController::ShowDivider() {
    329   divider_widget_->Show();
    330   if (!window_targeter_) {
    331     scoped_ptr<ui::EventTargeter> window_targeter =
    332         scoped_ptr<ui::EventTargeter>(new PriorityWindowTargeter(drag_handle_));
    333     window_targeter_.reset(
    334         new aura::ScopedWindowTargeter(container_, window_targeter.Pass()));
    335   }
    336 }
    337 
    338 gfx::Rect SplitViewController::GetLeftAreaBounds() {
    339   gfx::Rect work_area =
    340       gfx::Screen::GetNativeScreen()->GetPrimaryDisplay().work_area();
    341   return gfx::Rect(
    342       0, 0, divider_position_ - kDividerWidth / 2, work_area.height());
    343 }
    344 
    345 gfx::Rect SplitViewController::GetRightAreaBounds() {
    346   gfx::Rect work_area =
    347       gfx::Screen::GetNativeScreen()->GetPrimaryDisplay().work_area();
    348   int container_width = container_->bounds().width();
    349   return gfx::Rect(divider_position_ + kDividerWidth / 2,
    350                    0,
    351                    container_width - divider_position_ - kDividerWidth / 2,
    352                    work_area.height());
    353 }
    354 
    355 void SplitViewController::SetState(SplitViewController::State state) {
    356   if (state_ == state)
    357     return;
    358 
    359   if (divider_widget_ == NULL)
    360     InitializeDivider();
    361 
    362   state_ = state;
    363 
    364   ScreenManager::Get()->SetRotationLocked(state_ != INACTIVE);
    365   if (state == INACTIVE)
    366     HideDivider();
    367   else
    368     ShowDivider();
    369 }
    370 
    371 void SplitViewController::UpdateLayout(bool animate) {
    372   CHECK(left_window_);
    373   CHECK(right_window_);
    374   // Splitview can be activated from SplitViewController::ActivateSplitMode or
    375   // SplitViewController::ScrollEnd. Additionally we don't want to rotate the
    376   // screen while engaging splitview (i.e. state_ == SCROLLING).
    377   if (state_ == INACTIVE && !animate) {
    378     gfx::Rect work_area =
    379         gfx::Screen::GetNativeScreen()->GetPrimaryDisplay().work_area();
    380     aura::Window* top_window = window_list_provider_->GetWindowList().back();
    381     if (top_window != left_window_) {
    382       // TODO(mfomitchev): Use to_hide_ instead
    383       left_window_->Hide();
    384       right_window_->SetBounds(gfx::Rect(work_area.size()));
    385     }
    386     if (top_window != right_window_) {
    387       left_window_->SetBounds(gfx::Rect(work_area.size()));
    388       // TODO(mfomitchev): Use to_hide_ instead
    389       right_window_->Hide();
    390     }
    391     SetWindowTransforms(
    392         gfx::Transform(), gfx::Transform(), gfx::Transform(), false);
    393     return;
    394   }
    395 
    396   left_window_->Show();
    397   right_window_->Show();
    398 
    399   gfx::Transform divider_transform;
    400   divider_transform.Translate(divider_position_, 0);
    401   if (state_ == ACTIVE) {
    402     if (animate) {
    403       gfx::Transform left_transform =
    404           GetTransformForBounds(left_window_->bounds(), GetLeftAreaBounds());
    405       gfx::Transform right_transform =
    406           GetTransformForBounds(right_window_->bounds(), GetRightAreaBounds());
    407       SetWindowTransforms(
    408           left_transform, right_transform, divider_transform, true);
    409     } else {
    410       left_window_->SetBounds(GetLeftAreaBounds());
    411       right_window_->SetBounds(GetRightAreaBounds());
    412       SetWindowTransforms(
    413           gfx::Transform(), gfx::Transform(), divider_transform, false);
    414     }
    415   } else {
    416     gfx::Transform left_transform;
    417     gfx::Transform right_transform;
    418     gfx::Rect left_area_bounds = GetLeftAreaBounds();
    419     gfx::Rect right_area_bounds = GetRightAreaBounds();
    420     // If the width of the window is greater than the width of the area which it
    421     // is supposed to occupy - translate the window. Otherwise scale the window
    422     // up to fill the target area.
    423     if (left_window_->bounds().width() >= left_area_bounds.width()) {
    424       left_transform.Translate(
    425           left_area_bounds.right() - left_window_->bounds().right(), 0);
    426     } else {
    427       left_transform =
    428           GetTransformForBounds(left_window_->bounds(), left_area_bounds);
    429     }
    430     if (right_window_->bounds().width() >= right_area_bounds.width()) {
    431       right_transform.Translate(
    432           right_area_bounds.x() - right_window_->bounds().x(), 0);
    433     } else {
    434       right_transform =
    435           GetTransformForBounds(right_window_->bounds(), right_area_bounds);
    436     }
    437     SetWindowTransforms(
    438         left_transform, right_transform, divider_transform, animate);
    439   }
    440   // Note: |left_window_| and |right_window_| may be NULL if calling
    441   // SetWindowTransforms():
    442   // - caused the in-progress animation to abort.
    443   // - started a zero duration animation.
    444 }
    445 
    446 void SplitViewController::SetWindowTransforms(
    447     const gfx::Transform& left_transform,
    448     const gfx::Transform& right_transform,
    449     const gfx::Transform& divider_transform,
    450     bool animate) {
    451   if (animate) {
    452     ui::ScopedLayerAnimationSettings left_settings(
    453         left_window_->layer()->GetAnimator());
    454     left_settings.SetPreemptionStrategy(
    455         ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
    456     left_window_->SetTransform(left_transform);
    457 
    458     ui::ScopedLayerAnimationSettings divider_widget_settings(
    459         divider_widget_->GetNativeWindow()->layer()->GetAnimator());
    460     divider_widget_settings.SetPreemptionStrategy(
    461         ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
    462     divider_widget_->GetNativeWindow()->SetTransform(divider_transform);
    463 
    464     ui::ScopedLayerAnimationSettings right_settings(
    465         right_window_->layer()->GetAnimator());
    466     right_settings.SetPreemptionStrategy(
    467         ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
    468     right_settings.AddObserver(new ui::ClosureAnimationObserver(
    469         base::Bind(&SplitViewController::OnAnimationCompleted,
    470                    weak_factory_.GetWeakPtr())));
    471     right_window_->SetTransform(right_transform);
    472   } else {
    473     left_window_->SetTransform(left_transform);
    474     divider_widget_->GetNativeWindow()->SetTransform(divider_transform);
    475     right_window_->SetTransform(right_transform);
    476   }
    477 }
    478 
    479 void SplitViewController::OnAnimationCompleted() {
    480   // Animation can be cancelled when deactivated.
    481   if (left_window_ == NULL)
    482     return;
    483   UpdateLayout(false);
    484 
    485   for (size_t i = 0; i < to_hide_.size(); ++i)
    486     to_hide_[i]->Hide();
    487   to_hide_.clear();
    488 
    489   if (state_ == INACTIVE) {
    490     left_window_ = NULL;
    491     right_window_ = NULL;
    492   }
    493 }
    494 
    495 int SplitViewController::GetDefaultDividerPosition() {
    496   return container_->GetBoundsInScreen().width() / 2;
    497 }
    498 
    499 ///////////////////////////////////////////////////////////////////////////////
    500 // BezelController::ScrollDelegate:
    501 
    502 void SplitViewController::BezelScrollBegin(BezelController::Bezel bezel,
    503                                            float delta) {
    504   if (!BezelCanScroll())
    505     return;
    506 
    507   SetState(SCROLLING);
    508 
    509   const aura::Window::Windows& windows = window_list_provider_->GetWindowList();
    510   CHECK(windows.size() >= 2);
    511   aura::Window::Windows::const_reverse_iterator iter = windows.rbegin();
    512   aura::Window* current_window = *(iter);
    513 
    514   if (delta > 0) {
    515     right_window_ = current_window;
    516     left_window_ = *(iter + 1);
    517   } else {
    518     left_window_ = current_window;
    519     right_window_ = *(iter + 1);
    520   }
    521 
    522   CHECK(left_window_);
    523   CHECK(right_window_);
    524 
    525   // Calculate divider_scroll_start_position_
    526   gfx::Screen* screen = gfx::Screen::GetScreenFor(container_);
    527   const gfx::Rect& display_bounds =
    528       screen->GetDisplayNearestWindow(container_).bounds();
    529   gfx::Rect container_bounds = container_->GetBoundsInScreen();
    530   divider_scroll_start_position_ =
    531       delta > 0 ? display_bounds.x() - container_bounds.x()
    532                 : display_bounds.right() - container_bounds.x();
    533 
    534   divider_position_ = divider_scroll_start_position_ + delta;
    535   UpdateLayout(false);
    536 }
    537 
    538 void SplitViewController::BezelScrollEnd() {
    539   if (state_ != SCROLLING)
    540     return;
    541 
    542   // Max distance from the scroll end position to the middle of the screen where
    543   // we would go into the split view mode.
    544   const int kMaxDistanceFromMiddle = 120;
    545   const int default_divider_position = GetDefaultDividerPosition();
    546   if (std::abs(default_divider_position - divider_position_) <=
    547       kMaxDistanceFromMiddle) {
    548     divider_position_ = default_divider_position;
    549     SetState(ACTIVE);
    550   } else if (divider_position_ < default_divider_position) {
    551     divider_position_ = 0;
    552     SetState(INACTIVE);
    553     wm::ActivateWindow(right_window_);
    554   } else {
    555     divider_position_ = container_->GetBoundsInScreen().width();
    556     SetState(INACTIVE);
    557     wm::ActivateWindow(left_window_);
    558   }
    559   UpdateLayout(true);
    560 }
    561 
    562 void SplitViewController::BezelScrollUpdate(float delta) {
    563   if (state_ != SCROLLING)
    564     return;
    565   divider_position_ = divider_scroll_start_position_ + delta;
    566   UpdateLayout(false);
    567 }
    568 
    569 bool SplitViewController::BezelCanScroll() {
    570   return CanActivateSplitViewMode();
    571 }
    572 
    573 ///////////////////////////////////////////////////////////////////////////////
    574 // DragHandleScrollDelegate:
    575 
    576 void SplitViewController::HandleScrollBegin(float delta) {
    577   CHECK(state_ == ACTIVE);
    578   state_ = SCROLLING;
    579   divider_scroll_start_position_ = GetDefaultDividerPosition();
    580   divider_position_ = divider_scroll_start_position_ + delta;
    581   UpdateLayout(false);
    582 }
    583 
    584 void SplitViewController::HandleScrollEnd() {
    585   BezelScrollEnd();
    586 }
    587 
    588 void SplitViewController::HandleScrollUpdate(float delta) {
    589   BezelScrollUpdate(delta);
    590 }
    591 
    592 ///////////////////////////////////////////////////////////////////////////////
    593 // WindowManagerObserver:
    594 
    595 void SplitViewController::OnOverviewModeEnter() {
    596   if (divider_widget_)
    597     HideDivider();
    598 }
    599 
    600 void SplitViewController::OnOverviewModeExit() {
    601   if (state_ != INACTIVE)
    602     ShowDivider();
    603 }
    604 
    605 void SplitViewController::OnSplitViewModeEnter() {
    606 }
    607 
    608 void SplitViewController::OnSplitViewModeExit() {
    609 }
    610 
    611 }  // namespace athena
    612