chromium/ash/wm/client_controlled_state.cc

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/wm/client_controlled_state.h"

#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/wm/float/float_controller.h"
#include "ash/wm/pip/pip_positioner.h"
#include "ash/wm/screen_pinning_controller.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/window_positioning_utils.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_state_delegate.h"
#include "ash/wm/window_state_util.h"
#include "ash/wm/wm_metrics.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/wm/window_util.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/display/screen.h"
#include "ui/wm/core/window_util.h"

namespace ash {

namespace {

using ::chromeos::WindowStateType;

}  // namespace

ClientControlledState::ClientControlledState(std::unique_ptr<Delegate> delegate)
    : BaseState(WindowStateType::kDefault), delegate_(std::move(delegate)) {}

ClientControlledState::~ClientControlledState() = default;

void ClientControlledState::ResetDelegate() {
  delegate_.reset();
}

void ClientControlledState::HandleTransitionEvents(WindowState* window_state,
                                                   const WMEvent* event) {
  if (!delegate_)
    return;

  const WMEventType event_type = event->type();
  bool pin_transition = window_state->IsTrustedPinned() ||
                        window_state->IsPinned() || event->IsPinEvent();
  // Pinned State transition is handled on server side.
  if (pin_transition) {
    // Only one window can be pinned.
    if (event->IsPinEvent() &&
        Shell::Get()->screen_pinning_controller()->IsPinned()) {
      return;
    }
    WindowStateType next_state_type =
        GetStateForTransitionEvent(window_state, event);
    delegate_->HandleWindowStateRequest(window_state, next_state_type);
    return;
  }

  switch (event_type) {
    case WM_EVENT_NORMAL:
    case WM_EVENT_MAXIMIZE:
    case WM_EVENT_MINIMIZE:
    case WM_EVENT_FULLSCREEN:
    case WM_EVENT_SNAP_PRIMARY:
    case WM_EVENT_SNAP_SECONDARY:
    case WM_EVENT_FLOAT: {
      WindowStateType next_state =
          GetResolvedNextWindowStateType(window_state, event);
      UpdateWindowForTransitionEvents(window_state, next_state, event);
      break;
    }
    case WM_EVENT_RESTORE:
      UpdateWindowForTransitionEvents(
          window_state, window_state->GetRestoreWindowState(), event);
      break;
    case WM_EVENT_SHOW_INACTIVE:
      NOTREACHED();
    default:
      NOTREACHED() << "Unknown event :" << event->type();
  }
}

void ClientControlledState::AttachState(
    WindowState* window_state,
    WindowState::State* state_in_previous_mode) {
  window_state->is_client_controlled_ = true;
}

void ClientControlledState::DetachState(WindowState* window_state) {
  window_state->is_client_controlled_ = false;
}

void ClientControlledState::HandleWorkspaceEvents(WindowState* window_state,
                                                  const WMEvent* event) {
  if (!delegate_)
    return;
  aura::Window* const window = window_state->window();
  // Client is responsible for adjusting bounds after workspace bounds change.
  if (window_state->IsSnapped()) {
    // If `SplitViewController` is aware of `window` (e.g. in tablet), let the
    // controller handle the workspace event.
    if (SplitViewController::Get(window)->IsWindowInSplitView(window)) {
      return;
    }

    gfx::Rect bounds = window->bounds();
    window_state->AdjustSnappedBoundsForDisplayWorkspaceChange(&bounds);

    // Then ask delegate to set the desired bounds for the snap state.
    delegate_->HandleBoundsRequest(window_state, window_state->GetStateType(),
                                   bounds, window_state->GetDisplay().id());
  } else if (window_state->IsFloated()) {
    if (!window->parent()) {
      // If the window is now reparenting to another container (or being
      // destroyed), no need to adjust floated bounds. The next workspace event
      // (`WM_EVENT_ADDED_TO_WORKSPACE`) is coming soon anyway.
      return;
    }
    const gfx::Rect bounds =
        display::Screen::GetScreen()->InTabletMode()
            ? FloatController::GetFloatWindowTabletBounds(window)
            : FloatController::GetFloatWindowClamshellBounds(
                  window,
                  // TODO(b/292579250): Add a mechanism to float as close to the
                  // previous bounds in the event of a workspace event. For now,
                  // use the default float location.
                  chromeos::FloatStartLocation::kBottomRight);
    delegate_->HandleBoundsRequest(window_state, window_state->GetStateType(),
                                   bounds, window_state->GetDisplay().id());
  } else if (event->type() == WM_EVENT_DISPLAY_METRICS_CHANGED) {
    // Explicitly handle the primary change because it can change the display id
    // with no bounds change.
    if (event->AsDisplayMetricsChangedWMEvent()->primary_changed()) {
      const gfx::Rect bounds = window->bounds();
      delegate_->HandleBoundsRequest(window_state, window_state->GetStateType(),
                                     bounds, window_state->GetDisplay().id());
    }
  } else if (event->type() == WM_EVENT_ADDED_TO_WORKSPACE) {
    gfx::Rect bounds = window->bounds();
    AdjustBoundsToEnsureMinimumWindowVisibility(
        window->GetRootWindow()->bounds(), /*client_controlled=*/true, &bounds);

    if (window->bounds() != bounds)
      window_state->SetBoundsConstrained(bounds);
  }
}

void ClientControlledState::HandleCompoundEvents(WindowState* window_state,
                                                 const WMEvent* event) {
  if (!delegate_)
    return;
  switch (event->type()) {
    case WM_EVENT_TOGGLE_MAXIMIZE_CAPTION:
      ToggleMaximizeCaption(window_state);
      break;
    case WM_EVENT_TOGGLE_MAXIMIZE:
      ToggleMaximize(window_state);
      break;
    case WM_EVENT_TOGGLE_VERTICAL_MAXIMIZE:
      // TODO(oshima): Implement this.
      break;
    case WM_EVENT_TOGGLE_HORIZONTAL_MAXIMIZE:
      // TODO(oshima): Implement this.
      break;
    case WM_EVENT_TOGGLE_FULLSCREEN:
      ToggleFullScreen(window_state, window_state->delegate());
      break;
    case WM_EVENT_CYCLE_SNAP_PRIMARY:
    case WM_EVENT_CYCLE_SNAP_SECONDARY:
      CycleSnap(window_state, event->type());
      break;
    default:
      NOTREACHED() << "Invalid event :" << event->type();
  }
}

void ClientControlledState::HandleBoundsEvents(WindowState* window_state,
                                               const WMEvent* event) {
  if (!delegate_)
    return;
  auto* const window = window_state->window();
  switch (event->type()) {
    case WM_EVENT_SET_BOUNDS: {
      const auto* set_bounds_event = event->AsSetBoundsWMEvent();
      const gfx::Rect& bounds = set_bounds_event->requested_bounds_in_parent();
      if (set_bounds_locally_) {
        if (window_state->IsFloated()) {
          // Don’t preempt on-going animation (e.g. tucking) for floated
          // windows.
          if (window->layer() &&
              window->layer()->GetAnimator()->is_animating()) {
            return;
          }
          // Don't move the tucked window. It's fully controlled by ash now.
          if (Shell::Get()->float_controller()->IsFloatedWindowTuckedForTablet(
                  window)) {
            return;
          }
        }

        switch (next_bounds_change_animation_type_) {
          case WindowState::BoundsChangeAnimationType::kNone:
            window_state->SetBoundsDirect(bounds);
            break;
          case WindowState::BoundsChangeAnimationType::kCrossFade:
            window_state->SetBoundsDirectCrossFade(bounds);
            break;
          case WindowState::BoundsChangeAnimationType::kCrossFadeFloat:
            window_state->SetBoundsDirectCrossFade(bounds, true);
            break;
          case WindowState::BoundsChangeAnimationType::kCrossFadeUnfloat:
            window_state->SetBoundsDirectCrossFade(bounds, false);
            break;
          case WindowState::BoundsChangeAnimationType::kAnimate:
            window_state->SetBoundsDirectAnimated(
                bounds, bounds_change_animation_duration_);
            break;
          case WindowState::BoundsChangeAnimationType::kAnimateZero:
            NOTREACHED();
        }
        next_bounds_change_animation_type_ =
            WindowState::BoundsChangeAnimationType::kNone;
      } else if (!window_state->IsPinned()) {
        // TODO(oshima): Define behavior for pinned app.
        bounds_change_animation_duration_ = set_bounds_event->duration();
        int64_t display_id = set_bounds_event->display_id();
        if (display_id == display::kInvalidDisplayId) {
          display_id = display::Screen::GetScreen()
                           ->GetDisplayNearestWindow(window)
                           .id();
        }
#if DCHECK_IS_ON()
        gfx::Rect bounds_in_display(bounds);
        // The coordinates of the WindowState's parent must be same as display
        // coordinates. The following code is only to verify this condition.
        const aura::Window* root = window->GetRootWindow();
        aura::Window::ConvertRectToTarget(window->parent(), root,
                                          &bounds_in_display);
        DCHECK_EQ(bounds_in_display.x(), bounds.x());
        DCHECK_EQ(bounds_in_display.y(), bounds.y());
#endif
        delegate_->HandleBoundsRequest(
            window_state, window_state->GetStateType(), bounds, display_id);
      }
      break;
    }
    default:
      NOTREACHED() << "Unknown event:" << event->type();
  }
}

void ClientControlledState::OnWindowDestroying(WindowState* window_state) {
  ResetDelegate();
}

bool ClientControlledState::EnterNextState(WindowState* window_state,
                                           WindowStateType next_state_type) {
  // Do nothing if  we're already in the same state, or delegate has already
  // been deleted.
  if (state_type_ == next_state_type || !delegate_)
    return false;
  WindowStateType previous_state_type = state_type_;
  state_type_ = next_state_type;

  window_state->UpdateWindowPropertiesFromStateType();
  window_state->NotifyPreStateTypeChange(previous_state_type);

  auto* const window = window_state->window();

  // Calling order matters. We need to handle the floated state before handling
  // the minimized state because FloatController may change the visibility of
  // the window.
  auto* const float_controller = Shell::Get()->float_controller();
  if (next_state_type == WindowStateType::kFloated) {
    if (display::Screen::GetScreen()->InTabletMode()) {
      float_controller->FloatForTablet(window, previous_state_type);
    } else {
      float_controller->FloatImpl(window);
    }
  }
  if (previous_state_type == WindowStateType::kFloated) {
    float_controller->UnfloatImpl(window);
  }

  // Don't update the window if the window is detached from parent.
  // This can happen during dragging.
  // TODO(oshima): This was added for DOCKED windows. Investigate if
  // we still need this.
  if (window->parent()) {
    UpdateMinimizedState(window_state, previous_state_type);
  }

  window_state->NotifyPostStateTypeChange(previous_state_type);

  if (IsPinnedWindowStateType(next_state_type) ||
      IsPinnedWindowStateType(previous_state_type)) {
    set_next_bounds_change_animation_type(
        WindowState::BoundsChangeAnimationType::kCrossFade);
    Shell::Get()->screen_pinning_controller()->SetPinnedWindow(window);
  }
  return true;
}

WindowStateType ClientControlledState::GetResolvedNextWindowStateType(
    WindowState* window_state,
    const WMEvent* event) {
  DCHECK(event->IsTransitionEvent());

  const WindowStateType next = GetStateForTransitionEvent(window_state, event);

  if (display::Screen::GetScreen()->InTabletMode() &&
      next == WindowStateType::kNormal && window_state->CanMaximize()) {
    return WindowStateType::kMaximized;
  }

  return next;
}

void ClientControlledState::UpdateWindowForTransitionEvents(
    WindowState* window_state,
    chromeos::WindowStateType next_state_type,
    const WMEvent* event) {
  const WMEventType event_type = event->type();
  aura::Window* window = window_state->window();

  if (chromeos::IsSnappedWindowStateType(next_state_type)) {
    if (window_state->CanSnap()) {
      const bool is_restoring =
          window->GetProperty(aura::client::kIsRestoringKey) ||
          event_type == WM_EVENT_RESTORE;
      CHECK(is_restoring || event->IsSnapEvent());

      // If the window is being unminimized to any snapped state and it's still
      // transitioning, no need to handle the extra snap event.
      if (window_state->IsMinimized() && is_restoring &&
          SplitViewController::Get(window)->IsWindowInTransitionalState(
              window)) {
        return;
      }

      const WindowSnapActionSource snap_action_source =
          is_restoring ? WindowSnapActionSource::kSnapByWindowStateRestore
                       : event->AsSnapEvent()->snap_action_source();
      HandleWindowSnapping(window_state,
                           next_state_type == WindowStateType::kPrimarySnapped
                               ? WM_EVENT_SNAP_PRIMARY
                               : WM_EVENT_SNAP_SECONDARY,
                           snap_action_source);
      window_state->RecordWindowSnapActionSource(snap_action_source);

      // Get the desired window bounds for the snap state.
      // TODO(b/246683799): Investigate why window_state->snap_ratio() can be
      // empty.
      // Use the saved `window_state->snap_ratio()` if restoring, otherwise use
      // the event requested snap ratio, which has a default value.
      const float next_snap_ratio =
          is_restoring
              ? window_state->snap_ratio().value_or(chromeos::kDefaultSnapRatio)
              : event->AsSnapEvent()->snap_ratio();

      // We don't want Unminimize() to restore the pre-snapped state during the
      // transition. See crbug.com/1031313 for why we need this.
      // kRestoreShowStateKey property will be updated properly after the window
      // is snapped correctly.
      if (window_state->IsMinimized())
        window->ClearProperty(aura::client::kRestoreShowStateKey);

      window_state->UpdateWindowPropertiesFromStateType();
      VLOG(1) << "Processing State Transtion: event=" << event_type
              << ", state=" << state_type_
              << ", next_state=" << next_state_type;

      const gfx::Rect snapped_bounds = GetSnappedWindowBoundsInParent(
          window, next_state_type, next_snap_ratio);

      // The snap ratio of `snapped_bounds` may be different from the requested
      // snap ratio (e.g., if the window has a minimum size requirement or the
      // opposite side of splitview is partial-snapped).
      window_state->ForceUpdateSnapRatio(snapped_bounds);

      // Then ask delegate to set the desired bounds for the snap state.
      delegate_->HandleBoundsRequest(window_state, next_state_type,
                                     snapped_bounds,
                                     window_state->GetDisplay().id());
    }
  } else if (next_state_type == WindowStateType::kFloated) {
    if (chromeos::wm::CanFloatWindow(window)) {
      const gfx::Rect bounds =
          display::Screen::GetScreen()->InTabletMode()
              ? FloatController::GetFloatWindowTabletBounds(window)
              : FloatController::GetFloatWindowClamshellBounds(
                    window, event_type == WM_EVENT_FLOAT
                                ? event->AsFloatEvent()->float_start_location()
                                : chromeos::FloatStartLocation::kBottomRight);

      window_state->UpdateWindowPropertiesFromStateType();
      VLOG(1) << "Processing State Transtion: event=" << event_type
              << ", state=" << state_type_
              << ", next_state=" << next_state_type;

      // Then ask delegate to set the desired bounds for the float state.
      delegate_->HandleBoundsRequest(window_state, next_state_type, bounds,
                                     window_state->GetDisplay().id());
    }
  } else {
    // Clients handle a window state change asynchronously. So in the case
    // that the window is in a transitional state (already snapped but not
    // applied to its window state yet), we here skip to pass WM_EVENT.
    if (SplitViewController::Get(window)->IsWindowInTransitionalState(window))
      return;

    // Reset window state.
    window_state->UpdateWindowPropertiesFromStateType();
    VLOG(1) << "Processing State Transtion: event=" << event_type
            << ", state=" << state_type_ << ", next_state=" << next_state_type;

    // Then ask delegate to handle the window state change.
    delegate_->HandleWindowStateRequest(window_state, next_state_type);
  }
}

}  // namespace ash