chromium/ash/wm/workspace/workspace_layout_manager.cc

// Copyright 2012 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/workspace/workspace_layout_manager.h"

#include <algorithm>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/system/notification_center/ash_message_popup_collection.h"
#include "ash/wm/always_on_top_controller.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/fullscreen_window_finder.h"
#include "ash/wm/pip/pip_controller.h"
#include "ash/wm/screen_pinning_controller.h"
#include "ash/wm/window_positioner.h"
#include "ash/wm/window_properties.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/workspace/backdrop_controller.h"
#include "base/containers/adapters.h"
#include "base/containers/contains.h"
#include "base/memory/raw_ptr.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window_tracker.h"
#include "ui/compositor/layer.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/tablet_state.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/public/activation_client.h"

namespace ash {

using ::chromeos::WindowStateType;

WorkspaceLayoutManager::FloatingWindowObserver::FloatingWindowObserver(
    WorkspaceLayoutManager* workspace_layout_manager)
    : workspace_layout_manager_(workspace_layout_manager) {}

WorkspaceLayoutManager::FloatingWindowObserver::~FloatingWindowObserver() =
    default;

void WorkspaceLayoutManager::FloatingWindowObserver::MaybeObserveWindow(
    aura::Window* window) {
  if (window_observations_.IsObservingSource(window)) {
    return;
  }

  aura::Window* root = window->GetRootWindow();
  aura::Window* parent = window->parent();
  if (parent == root->GetChildById(kShellWindowId_SettingBubbleContainer) ||
      parent ==
          root->GetChildById(kShellWindowId_AccessibilityBubbleContainer) ||
      (parent == root->GetChildById(kShellWindowId_ShelfContainer) &&
       window->GetName() ==
           AshMessagePopupCollection::kMessagePopupWidgetName)) {
    window_observations_.AddObservation(window);
  }
}

void WorkspaceLayoutManager::FloatingWindowObserver::OnWindowHierarchyChanged(
    const HierarchyChangeParams& params) {
  if (window_observations_.IsObservingSource(params.target) &&
      (!params.new_parent || params.new_parent != params.old_parent)) {
    window_observations_.RemoveObservation(params.target);
  }
}

void WorkspaceLayoutManager::FloatingWindowObserver::OnWindowVisibilityChanged(
    aura::Window* window,
    bool visible) {
  if (window_observations_.IsObservingSource(window)) {
    workspace_layout_manager_->MaybeUpdateA11yFloatingPanelOrPipBounds();
  }
}

void WorkspaceLayoutManager::FloatingWindowObserver::OnWindowDestroying(
    aura::Window* window) {
  workspace_layout_manager_->MaybeUpdateA11yFloatingPanelOrPipBounds();
  window_observations_.RemoveObservation(window);
}

void WorkspaceLayoutManager::FloatingWindowObserver::OnWindowBoundsChanged(
    aura::Window* window,
    const gfx::Rect& old_bounds,
    const gfx::Rect& new_bounds,
    ui::PropertyChangeReason reason) {
  workspace_layout_manager_->MaybeUpdateA11yFloatingPanelOrPipBounds();
}

WorkspaceLayoutManager::WorkspaceLayoutManager(aura::Window* window)
    : window_(window),
      root_window_(window->GetRootWindow()),
      root_window_controller_(RootWindowController::ForWindow(root_window_)),
      is_fullscreen_(GetWindowForFullscreenModeForContext(window) != nullptr),
      floating_window_observer_(
          std::make_unique<FloatingWindowObserver>(this)) {
  Shell::Get()->AddShellObserver(this);
  Shell::Get()->activation_client()->AddObserver(this);
  root_window_->AddObserver(this);
  backdrop_controller_ = std::make_unique<BackdropController>(window_);
  keyboard::KeyboardUIController::Get()->AddObserver(this);
  root_window_controller_->shelf()->AddObserver(this);
  Shell::Get()->app_list_controller()->AddObserver(this);
}

WorkspaceLayoutManager::~WorkspaceLayoutManager() {
  // WorkspaceLayoutManagers for the primary display are destroyed after
  // AppListControllerImpl. Their observers are removed in OnShellDestroying().
  if (Shell::Get()->app_list_controller())
    Shell::Get()->app_list_controller()->RemoveObserver(this);
  root_window_controller_->shelf()->RemoveObserver(this);
  if (root_window_)
    root_window_->RemoveObserver(this);
  for (aura::Window* window : windows_) {
    WindowState* window_state = WindowState::Get(window);
    window_state->RemoveObserver(this);
    window->RemoveObserver(this);
  }
  Shell::Get()->activation_client()->RemoveObserver(this);
  Shell::Get()->RemoveShellObserver(this);
  keyboard::KeyboardUIController::Get()->RemoveObserver(this);
}

//////////////////////////////////////////////////////////////////////////////
// WorkspaceLayoutManager, aura::LayoutManager implementation:

void WorkspaceLayoutManager::OnWindowResized() {}

void WorkspaceLayoutManager::OnWindowAddedToLayout(aura::Window* child) {
  DCHECK_NE(aura::client::WINDOW_TYPE_CONTROL, child->GetType());
  WindowState* window_state = WindowState::Get(child);
  WMEvent event(WM_EVENT_ADDED_TO_WORKSPACE);
  window_state->OnWMEvent(&event);
  windows_.insert(child);
  child->AddObserver(this);
  window_state->AddObserver(this);
  UpdateShelfVisibility();
  UpdateFullscreenState();
  UpdateWindowWorkspace(child);

  backdrop_controller_->OnWindowAddedToLayout(child);
  window_positioner::RearrangeVisibleWindowOnShow(child);
  if (Shell::Get()->screen_pinning_controller()->IsPinned())
    WindowState::Get(child)->DisableZOrdering(nullptr);
}

void WorkspaceLayoutManager::OnWillRemoveWindowFromLayout(aura::Window* child) {
  windows_.erase(child);
  child->RemoveObserver(this);
  WindowState* window_state = WindowState::Get(child);
  window_state->RemoveObserver(this);

  // When a window is removing from a workspace layout, it is going to be added
  // to a new workspace layout or destroyed.
  if (!window_state->pre_added_to_workspace_window_bounds()) {
    if (window_state->pre_auto_manage_window_bounds()) {
      window_state->set_pre_added_to_workspace_window_bounds(
          *window_state->pre_auto_manage_window_bounds());
    } else {
      window_state->set_pre_added_to_workspace_window_bounds(child->bounds());
    }
  }

  if (child->layer()->GetTargetVisibility())
    window_positioner::RearrangeVisibleWindowOnHideOrRemove(child);
}

void WorkspaceLayoutManager::OnWindowRemovedFromLayout(aura::Window* child) {
  UpdateShelfVisibility();
  UpdateFullscreenState();
  backdrop_controller_->OnWindowRemovedFromLayout(child);
}

void WorkspaceLayoutManager::OnChildWindowVisibilityChanged(aura::Window* child,
                                                            bool visible) {
  WindowState* window_state = WindowState::Get(child);
  // Attempting to show a minimized window. Unminimize it.
  if (visible && window_state->IsMinimized())
    window_state->Unminimize();

  if (child->layer()->GetTargetVisibility())
    window_positioner::RearrangeVisibleWindowOnShow(child);
  else
    window_positioner::RearrangeVisibleWindowOnHideOrRemove(child);
  UpdateFullscreenState();
  UpdateShelfVisibility();
  backdrop_controller_->OnChildWindowVisibilityChanged(child);
}

void WorkspaceLayoutManager::SetChildBounds(aura::Window* child,
                                            const gfx::Rect& requested_bounds) {
  WindowState* window_state = WindowState::Get(child);
  SetBoundsWMEvent event(requested_bounds);
  window_state->OnWMEvent(&event);
}

//////////////////////////////////////////////////////////////////////////////
// WorkspaceLayoutManager, aura::WindowObserver implementation:

void WorkspaceLayoutManager::OnWindowHierarchyChanged(
    const HierarchyChangeParams& params) {
  if (params.new_parent) {
    if (floating_window_observer_) {
      floating_window_observer_->MaybeObserveWindow(params.target);
    }

    // The window should have a parent (unless it's being removed), so we can
    // create WindowState, which requires its parent. (crbug.com/924305)
    WindowState::Get(params.target);
  }

  if (!wm::IsActiveWindow(params.target)) {
    return;
  }
  // If the window is already tracked by the workspace this update would be
  // redundant as the fullscreen and shelf state would have been handled in
  // OnWindowAddedToLayout.
  if (base::Contains(windows_, params.target)) {
    return;
  }

  // If the active window has moved to this root window then update the
  // fullscreen state.
  // TODO(flackr): Track the active window leaving this root window and update
  // the fullscreen state accordingly.
  if (params.new_parent && params.new_parent->GetRootWindow() == root_window_) {
    UpdateFullscreenState();
    UpdateShelfVisibility();
  }
}

void WorkspaceLayoutManager::OnWindowAdded(aura::Window* window) {
  if (floating_window_observer_) {
    floating_window_observer_->MaybeObserveWindow(window);
  }
}

void WorkspaceLayoutManager::OnWindowPropertyChanged(aura::Window* window,
                                                     const void* key,
                                                     intptr_t old) {
  if (key == aura::client::kZOrderingKey) {
    if (window->GetProperty(aura::client::kZOrderingKey) !=
        ui::ZOrderLevel::kNormal) {
      aura::Window* container =
          root_window_controller_->always_on_top_controller()->GetContainer(
              window);
      if (window->parent() != container)
        container->AddChild(window);
    }
  } else if (key == kWindowBackdropKey) {
    // kWindowBackdropKey is not supposed to be cleared.
    DCHECK(window->GetProperty(kWindowBackdropKey));
  } else if (key == aura::client::kWindowWorkspaceKey) {
    UpdateWindowWorkspace(window);
  }
}

void WorkspaceLayoutManager::OnWindowStackingChanged(aura::Window* window) {
  UpdateShelfVisibility();
  UpdateFullscreenState();
  backdrop_controller_->OnWindowStackingChanged(window);
}

void WorkspaceLayoutManager::OnWindowDestroying(aura::Window* window) {
  if (root_window_ == window) {
    root_window_->RemoveObserver(this);
    root_window_ = nullptr;
  }

  Shell::Get()->desks_controller()->MaybeRemoveVisibleOnAllDesksWindow(window);
}

//////////////////////////////////////////////////////////////////////////////
// WorkspaceLayoutManager, wm::ActivationChangeObserver implementation:

void WorkspaceLayoutManager::OnWindowActivating(ActivationReason reason,
                                                aura::Window* gaining_active,
                                                aura::Window* losing_active) {
  if (!base::Contains(windows_, gaining_active)) {
    return;
  }

  WindowState* window_state =
      gaining_active ? WindowState::Get(gaining_active) : nullptr;
  if (window_state && window_state->IsMinimized() &&
      !gaining_active->IsVisible()) {
    window_state->Unminimize();
  }
}

void WorkspaceLayoutManager::OnWindowActivated(ActivationReason reason,
                                               aura::Window* gained_active,
                                               aura::Window* lost_active) {
  // This callback may be called multiple times with one activation change
  // because we have one instance of this class for each desk.
  // TODO(http://b/265746505): Make sure to avoid redundant calls.
  if (lost_active)
    WindowState::Get(lost_active)->OnActivationLost();

  UpdateFullscreenState();
  UpdateShelfVisibility();
}

//////////////////////////////////////////////////////////////////////////////
// WorkspaceLayoutManager, ash::KeyboardControllerObserver implementation:

void WorkspaceLayoutManager::OnKeyboardVisibleBoundsChanged(
    const gfx::Rect& new_bounds) {
  auto* keyboard_window =
      keyboard::KeyboardUIController::Get()->GetKeyboardWindow();
  if (keyboard_window && keyboard_window->GetRootWindow() == root_window_) {
    MaybeUpdateA11yFloatingPanelOrPipBounds();
  }
}

void WorkspaceLayoutManager::OnKeyboardDisplacingBoundsChanged(
    const gfx::Rect& new_bounds_in_screen) {
  aura::Window* window = window_util::GetActiveWindow();
  if (!window) {
    return;
  }

  window = window->GetToplevelWindow();
  if (!window_->Contains(window)) {
    return;
  }

  WindowState* window_state = WindowState::Get(window);
  if (window_state->ignore_keyboard_bounds_change()) {
    return;
  }

  if (!new_bounds_in_screen.IsEmpty()) {
    // Store existing bounds to be restored before resizing for keyboard if it
    // is not already stored.
    if (!window_state->HasRestoreBounds()) {
      window_state->SaveCurrentBoundsForRestore();
    }

    gfx::Rect window_bounds(window->GetTargetBounds());
    wm::ConvertRectToScreen(window_, &window_bounds);
    const int vertical_displacement =
        std::max(0, window_bounds.bottom() - new_bounds_in_screen.y());
    const int shift = std::min(
        vertical_displacement,
        window_bounds.y() -
            screen_util::GetDisplayWorkAreaBoundsInParent(window_).y());
    if (shift > 0) {
      const gfx::Point origin(window_bounds.x(), window_bounds.y() - shift);
      SetChildBounds(window, gfx::Rect(origin, window_bounds.size()));
    }
  } else if (window_state->IsNormalStateType() &&
             window_state->HasRestoreBounds()) {
    // Keyboard hidden, restore original bounds if they exist. If the user has
    // resized or dragged the window in the meantime, WorkspaceWindowResizer
    // will have cleared the restore bounds and this code will not accidentally
    // override user intent. Only do this for normal window states that use the
    // restore bounds.
    window_state->SetAndClearRestoreBounds();
  }
}

//////////////////////////////////////////////////////////////////////////////
// WorkspaceLayoutManager, WindowStateObserver implementation:

void WorkspaceLayoutManager::OnPostWindowStateTypeChange(
    WindowState* window_state,
    WindowStateType old_type) {
  // Notify observers that fullscreen state may be changing.
  if (window_state->IsFullscreen() ||
      old_type == WindowStateType::kFullscreen) {
    UpdateFullscreenState();
  }

  UpdateShelfVisibility();
  backdrop_controller_->OnPostWindowStateTypeChange(window_state->window());
}

//////////////////////////////////////////////////////////////////////////////
// WorkspaceLayoutManager, display::DisplayObserver implementation:

void WorkspaceLayoutManager::OnDisplayMetricsChanged(
    const display::Display& display,
    uint32_t changed_metrics) {
  if (display::Screen::GetScreen()->GetDisplayNearestWindow(window_).id() !=
      display.id()) {
    return;
  }

  if (changed_metrics & (display::DisplayObserver::DISPLAY_METRIC_BOUNDS |
                         display::DisplayObserver::DISPLAY_METRIC_PRIMARY |
                         display::DisplayObserver::DISPLAY_METRIC_WORK_AREA)) {
    const DisplayMetricsChangedWMEvent wm_event(changed_metrics);
    AdjustAllWindowsBoundsForWorkAreaChange(&wm_event);
  }

  backdrop_controller_->OnDisplayMetricsChanged();
}

void WorkspaceLayoutManager::OnDisplayTabletStateChanged(
    display::TabletState state) {
  if (display::IsTabletStateChanging(state)) {
    // Do nothing if the tablet state is still in the process of transition.
    return;
  }

  backdrop_controller_->OnTabletModeChanged();
}

//////////////////////////////////////////////////////////////////////////////
// WorkspaceLayoutManager, ShellObserver implementation:

void WorkspaceLayoutManager::OnFullscreenStateChanged(bool is_fullscreen,
                                                      aura::Window* container) {
  // Note that only the active desk's container broadcasts this event, but all
  // containers' workspaces (active desk's and inactive desks' as well the
  // always-on-top container) receive it.
  DCHECK(desks_util::IsActiveDeskContainer(container));

  // If |container| is the one associated with this workspace, then fullscreen
  // state must match.
  DCHECK(window_ != container || is_fullscreen == is_fullscreen_);

  // This notification may come from active desk containers on other displays.
  // No need to update the always-on-top states if the fullscreen state change
  // happened on a different root window.
  if (container->GetRootWindow() != root_window_)
    return;

  if (Shell::Get()->screen_pinning_controller()->IsPinned()) {
    // If this is in pinned mode, then this event does not trigger the
    // always-on-top state change, because it is kept disabled regardless of
    // the fullscreen state change.
    return;
  }

  // We need to update the always-on-top states even for inactive desks
  // containers, because inactive desks may have a previously demoted
  // always-on-top windows that we need to promote back to the always-on-top
  // container if there no longer fullscreen windows on this root window.
  UpdateAlwaysOnTop(GetWindowForFullscreenModeInRoot(root_window_));
}

void WorkspaceLayoutManager::OnPinnedStateChanged(aura::Window* pinned_window) {
  const bool is_pinned = Shell::Get()->screen_pinning_controller()->IsPinned();
  if (!is_pinned && is_fullscreen_) {
    // On exiting from pinned mode, if the workspace is still in fullscreen
    // mode, then this event does not trigger the restoring yet. On exiting
    // from fullscreen, the temporarily disabled always-on-top property will be
    // restored.
    return;
  }

  UpdateAlwaysOnTop(is_pinned ? pinned_window : nullptr);
}

void WorkspaceLayoutManager::OnShellDestroying() {
  is_shell_destroying_ = true;
  Shell::Get()->app_list_controller()->RemoveObserver(this);
  floating_window_observer_.reset();
}

//////////////////////////////////////////////////////////////////////////////
// WorkspaceLayoutManager, ShelfObserver implementation:

void WorkspaceLayoutManager::OnAutoHideStateChanged(
    ShelfAutoHideState new_state) {
  MaybeUpdateA11yFloatingPanelOrPipBounds();
}

void WorkspaceLayoutManager::OnHotseatStateChanged(HotseatState old_state,
                                                   HotseatState new_state) {
  MaybeUpdateA11yFloatingPanelOrPipBounds();
}

//////////////////////////////////////////////////////////////////////////////
// WorkspaceLayoutManager, AppListControllerObserver implementation:

void WorkspaceLayoutManager::OnAppListVisibilityChanged(bool shown,
                                                        int64_t display_id) {
  if (display::Screen::GetScreen()->GetDisplayNearestWindow(window_).id() !=
      display_id) {
    return;
  }
  if (!Shell::Get()->IsInTabletMode()) {
    MaybeUpdateA11yFloatingPanelOrPipBounds();
  }
}

//////////////////////////////////////////////////////////////////////////////
// WorkspaceLayoutManager, private:

void WorkspaceLayoutManager::AdjustAllWindowsBoundsForWorkAreaChange(
    const WMEvent* event) {
  CHECK_EQ(WM_EVENT_DISPLAY_METRICS_CHANGED, event->type());

  const DisplayMetricsChangedWMEvent* display_event =
      event->AsDisplayMetricsChangedWMEvent();
  CHECK(display_event->display_bounds_changed() ||
        display_event->primary_changed() || display_event->work_area_changed());

  MaybeUpdateA11yFloatingPanelOrPipBounds();

  // If a user plugs an external display into a laptop running Aura the
  // display size will change.  Maximized windows need to resize to match.
  // We also do this when developers running Aura on a desktop manually resize
  // the host window.
  // We also need to do this when the work area insets changes.
  // Update the windows from top-most to bottom-most so when windows get bigger
  // they occlude windows below them first.
  auto ordered_windows = window_util::SortWindowsBottomToTop(windows_);
  for (aura::Window* window : base::Reversed(ordered_windows)) {
    WindowState::Get(window)->OnWMEvent(event);
  }
}

void WorkspaceLayoutManager::UpdateShelfVisibility() {
  root_window_controller_->shelf()->UpdateVisibilityState();
}

void WorkspaceLayoutManager::UpdateFullscreenState() {
  // Note that we don't allow always-on-top or PiP containers to have fullscreen
  // windows, and we only update the fullscreen state for the active desk
  // container.
  if (!desks_util::IsActiveDeskContainer(window_))
    return;

  const bool is_fullscreen =
      GetWindowForFullscreenModeForContext(window_) != nullptr;
  if (is_fullscreen == is_fullscreen_)
    return;

  is_fullscreen_ = is_fullscreen;
  Shell::Get()->NotifyFullscreenStateChanged(is_fullscreen, window_);
}

void WorkspaceLayoutManager::UpdateAlwaysOnTop(
    aura::Window* active_desk_fullscreen_window) {
  // Changing always on top state may change window's parent. Iterate on a copy
  // of |windows_| to avoid invalidating an iterator. Since both workspace and
  // always_on_top containers' layouts are managed by this class all the
  // appropriate windows will be included in the iteration.
  // Use an `aura::WindowTracker` since `OnWillRemoveWindowFromLayout()` may
  // remove windows from `windows_`.
  std::vector<raw_ptr<aura::Window, VectorExperimental>> windows(
      windows_.begin(), windows_.end());
  aura::WindowTracker tracker(windows);
  while (!tracker.windows().empty()) {
    aura::Window* window = tracker.Pop();
    if (window == active_desk_fullscreen_window)
      continue;

    WindowState* window_state = WindowState::Get(window);
    if (active_desk_fullscreen_window)
      window_state->DisableZOrdering(active_desk_fullscreen_window);
    else
      window_state->RestoreZOrdering();
  }
}

void WorkspaceLayoutManager::MaybeUpdateA11yFloatingPanelOrPipBounds() const {
  // The PIP avoids the accessibility bubble, so here we update the
  // accessibility bubble position first, so that if the PIP is also being shown
  // the PIPs calculation does not need to take place twice.
  if (!is_shell_destroying_) {
    Shell::Get()
        ->accessibility_controller()
        ->UpdateFloatingPanelBoundsIfNeeded();
  }
  for (aura::Window* window : windows_) {
    WindowState* window_state = WindowState::Get(window);
    if (window_state->IsPip()) {
      Shell::Get()->pip_controller()->UpdatePipBounds();
    }
  }
}

void WorkspaceLayoutManager::UpdateWindowWorkspace(aura::Window* window) {
  if (window->GetType() != aura::client::WindowType::WINDOW_TYPE_NORMAL ||
      window->GetProperty(aura::client::kZOrderingKey) !=
          ui::ZOrderLevel::kNormal) {
    return;
  }

  auto* desks_controller = Shell::Get()->desks_controller();
  if (desks_util::IsWindowVisibleOnAllWorkspaces(window))
    desks_controller->AddVisibleOnAllDesksWindow(window);
  else
    desks_controller->MaybeRemoveVisibleOnAllDesksWindow(window);
}

}  // namespace ash