910e62b5创建于 1月15日历史提交
// 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/frame/frame_view_ash.h"

#include <memory>

#include "ash/public/cpp/tablet_mode_observer.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_state_observer.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_highlight_border_overlay_delegate.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/frame/caption_buttons/frame_caption_button_container_view.h"
#include "chromeos/ui/frame/frame_utils.h"
#include "chromeos/ui/frame/frame_view_chromeos.h"
#include "chromeos/ui/frame/header_view.h"
#include "chromeos/ui/frame/immersive/immersive_fullscreen_controller.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/aura/window_observer.h"
#include "ui/base/hit_test.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/display/display_observer.h"
#include "ui/display/tablet_state.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/views/context_menu_controller.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/view.h"
#include "ui/views/view_targeter.h"
#include "ui/views/widget/widget.h"

DEFINE_UI_CLASS_PROPERTY_TYPE(ash::FrameViewAsh*)

namespace ash {

using ::chromeos::ImmersiveFullscreenController;
using ::chromeos::kFrameActiveColorKey;
using ::chromeos::kFrameInactiveColorKey;
using ::chromeos::kImmersiveImpliedByFullscreen;
using ::chromeos::kTrackDefaultFrameColors;
using ::chromeos::WindowStateType;

DEFINE_UI_CLASS_PROPERTY_KEY(FrameViewAsh*, kFrameViewAshKey, nullptr)

// This helper enables and disables immersive mode in response to state such as
// tablet mode and fullscreen changing. For legacy reasons, it's only
// instantiated for windows that have no WindowStateDelegate provided.
class FrameViewAshImmersiveHelper : public WindowStateObserver,
                                    public aura::WindowObserver,
                                    public display::DisplayObserver {
 public:
  FrameViewAshImmersiveHelper(views::Widget* widget,
                              FrameViewAsh* custom_frame_view)
      : widget_(widget),
        window_state_(WindowState::Get(widget->GetNativeWindow())) {
    window_state_->window()->AddObserver(this);
    window_state_->AddObserver(this);

    immersive_fullscreen_controller_ =
        std::make_unique<ImmersiveFullscreenController>();
    custom_frame_view->InitImmersiveFullscreenControllerForView(
        immersive_fullscreen_controller_.get());
  }
  FrameViewAshImmersiveHelper(const FrameViewAshImmersiveHelper&) = delete;
  FrameViewAshImmersiveHelper& operator=(const FrameViewAshImmersiveHelper&) =
      delete;

  ~FrameViewAshImmersiveHelper() override {
    if (window_state_) {
      window_state_->RemoveObserver(this);
      window_state_->window()->RemoveObserver(this);
    }
  }

  // display::DisplayObserver:
  void OnDisplayTabletStateChanged(display::TabletState state) override {
    if (!window_state_ || window_state_->IsFullscreen()) {
      return;
    }

    switch (state) {
      case display::TabletState::kEnteringTabletMode:
      case display::TabletState::kExitingTabletMode:
        break;
      case display::TabletState::kInTabletMode:
        if (Shell::Get()->tablet_mode_controller()->ShouldAutoHideTitlebars(
                widget_) &&
            !window_state_->IsFloated()) {
          ImmersiveFullscreenController::EnableForWidget(widget_, true);
        }
        break;
      case display::TabletState::kInClamshellMode:
        ImmersiveFullscreenController::EnableForWidget(widget_, false);
        break;
    }
  }

 private:
  // aura::WindowObserver:
  void OnWindowDestroying(aura::Window* window) override {
    window_state_->RemoveObserver(this);
    window->RemoveObserver(this);
    window_state_ = nullptr;
  }

  // WindowStateObserver:
  void OnPostWindowStateTypeChange(WindowState* window_state,
                                   WindowStateType old_type) override {
    views::Widget* widget =
        views::Widget::GetWidgetForNativeWindow(window_state->window());
    if (immersive_fullscreen_controller_ &&
        Shell::Get()->tablet_mode_controller() &&
        Shell::Get()->tablet_mode_controller()->ShouldAutoHideTitlebars(
            widget)) {
      if (window_state->IsMinimized() || window_state->IsFloated())
        ImmersiveFullscreenController::EnableForWidget(widget_, false);
      else if (window_state->IsMaximized())
        ImmersiveFullscreenController::EnableForWidget(widget_, true);
      return;
    }

    if (!window_state->IsFullscreen() && !window_state->IsMinimized())
      ImmersiveFullscreenController::EnableForWidget(widget_, false);

    if (window_state->IsFullscreen() &&
        window_state->window()->GetProperty(kImmersiveImpliedByFullscreen)) {
      ImmersiveFullscreenController::EnableForWidget(widget_, true);
    }
  }

  raw_ptr<views::Widget> widget_;
  raw_ptr<WindowState> window_state_;
  std::unique_ptr<ImmersiveFullscreenController>
      immersive_fullscreen_controller_;
  display::ScopedDisplayObserver display_observer_{this};
};

FrameViewAsh::FrameViewAsh(views::Widget* widget)
    : chromeos::FrameViewChromeOS(widget),
      frame_context_menu_controller_(
          std::make_unique<FrameContextMenuController>(widget, this)) {
  header_view_->set_immersive_mode_changed_callback(base::BindRepeating(
      &FrameViewAsh::InvalidateLayout, weak_factory_.GetWeakPtr(),
      // This will always be on a fresh call stack, never mid-layout so the
      // value passed here doesn't matter.
      /*avoid_propagate_during_layout=*/false));

  aura::Window* frame_window = widget->GetNativeWindow();
  window_util::InstallResizeHandleWindowTargeterForWindow(frame_window);

  // A delegate may be set which takes over the responsibilities of the
  // FrameViewAshImmersiveHelper. This is the case for container apps
  // such as ARC++, and in some tests.
  WindowState* window_state = WindowState::Get(frame_window);
  // A window may be created as a child window of the toplevel (captive portal).
  // TODO(oshima): It should probably be a transient child rather than normal
  // child. Investigate if we can remove this check.
  if (window_state && !window_state->HasDelegate()) {
    immersive_helper_ =
        std::make_unique<FrameViewAshImmersiveHelper>(widget, this);
  }

  frame_window->SetProperty(kFrameViewAshKey, this);
  if (!frame_window->GetProperty(aura::client::kWindowRoundedCornersKey)) {
    frame_window->SetProperty(aura::client::kWindowRoundedCornersKey,
                              chromeos::GetWindowRoundedCorners());
  }

  window_observation_.Observe(frame_window);

  const bool remove_standard_frame =
      frame_window->GetProperty(aura::client::kRemoveStandardFrame);
  SetFrameEnabled(!remove_standard_frame);

  header_view_->set_context_menu_controller(
      frame_context_menu_controller_.get());
}

FrameViewAsh::~FrameViewAsh() {
  header_view_->set_context_menu_controller(nullptr);
}

// static
FrameViewAsh* FrameViewAsh::Get(aura::Window* window) {
  return window->GetProperty(kFrameViewAshKey);
}

void FrameViewAsh::InitImmersiveFullscreenControllerForView(
    ImmersiveFullscreenController* immersive_fullscreen_controller) {
  immersive_fullscreen_controller->Init(GetHeaderView(), widget_,
                                        GetHeaderView());
}

void FrameViewAsh::SetFrameColors(SkColor active_frame_color,
                                  SkColor inactive_frame_color) {
  aura::Window* frame_window = widget_->GetNativeWindow();
  frame_window->SetProperty(kTrackDefaultFrameColors, false);
  frame_window->SetProperty(kFrameActiveColorKey, active_frame_color);
  frame_window->SetProperty(kFrameInactiveColorKey, inactive_frame_color);
}

void FrameViewAsh::SetCaptionButtonModel(
    std::unique_ptr<chromeos::CaptionButtonModel> model) {
  header_view_->caption_button_container()->SetModel(std::move(model));
  header_view_->UpdateCaptionButtons();
}

gfx::Rect FrameViewAsh::GetClientBoundsForWindowBounds(
    const gfx::Rect& window_bounds) const {
  gfx::Rect client_bounds(window_bounds);
  client_bounds.Inset(gfx::Insets::TLBR(NonClientTopBorderHeight(), 0, 0, 0));
  return client_bounds;
}

bool FrameViewAsh::ShouldShowContextMenu(
    views::View* source,
    const gfx::Point& screen_coords_point) {
  if (header_view_->in_immersive_mode()) {
    // If the `header_view_` is in immersive mode, then a `NonClientHitTest`
    // will return HTCLIENT so manually check whether `point` lies inside
    // `header_view_`.
    gfx::Point point_in_header_coords(screen_coords_point);
    views::View::ConvertPointToTarget(this, GetHeaderView(),
                                      &point_in_header_coords);
    return header_view_->HitTestRect(
        gfx::Rect(point_in_header_coords, gfx::Size(1, 1)));
  }

  // Only show the context menu if `screen_coords_point` is in the caption area.
  gfx::Point point_in_view_coords(screen_coords_point);
  views::View::ConvertPointFromScreen(this, &point_in_view_coords);
  return NonClientHitTest(point_in_view_coords) == HTCAPTION;
}

void FrameViewAsh::SetShouldPaintHeader(bool paint) {
  header_view_->SetShouldPaintHeader(paint);
}

int FrameViewAsh::NonClientTopBorderPreferredHeight() const {
  return header_view_->GetPreferredHeight();
}

const views::View* FrameViewAsh::GetAvatarIconViewForTest() const {
  return header_view_->avatar_icon();
}

SkColor FrameViewAsh::GetActiveFrameColorForTest() const {
  return widget_->GetNativeWindow()->GetProperty(kFrameActiveColorKey);
}

SkColor FrameViewAsh::GetInactiveFrameColorForTest() const {
  return widget_->GetNativeWindow()->GetProperty(kFrameInactiveColorKey);
}

void FrameViewAsh::SetFrameEnabled(bool enabled) {
  if (enabled == frame_enabled_)
    return;

  frame_enabled_ = enabled;
  overlay_view_->SetVisible(frame_enabled_);
  header_view_->SetShouldPaintHeader(frame_enabled_);
  UpdateWindowRoundedCorners();
  InvalidateLayout();
}

void FrameViewAsh::SetFrameOverlapped(bool overlapped) {
  if (overlapped == frame_overlapped_) {
    return;
  }

  bool fills_bounds_opaquely = true;
  if (overlapped) {
    // When frame is overlapped with the window area, we need to draw header
    // view in front of client content.
    // TODO(b/282627319): remove the layer at the right condition.
    header_view_->SetPaintToLayer();
    header_view_->layer()->parent()->StackAtTop(header_view_->layer());

    // Overlapped frames are now painted onto a dedicated header view layer
    // instead of the non-opaque layer that hosts the widget.
    // For windows that have rounded corners, the upper corners of the header
    // are rounded while the compositor still thinks that the layer fills the
    // whole rect, including the two upper corners.
    // Therefore, the header view layer also needs to be non-opaque to prevent
    // visual artifacts from appearing around the upper corners.
    const aura::Window* window = widget_->GetNativeWindow();
    if (WindowState::Get(window)->ShouldWindowHaveRoundedCorners()) {
      fills_bounds_opaquely = false;
    }
  }
  if (header_view_->layer()) {
    header_view_->layer()->SetFillsBoundsOpaquely(fills_bounds_opaquely);
  }

  frame_overlapped_ = overlapped;
  InvalidateLayout();
}

void FrameViewAsh::SetToggleResizeLockMenuCallback(
    base::RepeatingCallback<void()> callback) {
  toggle_resize_lock_menu_callback_ = std::move(callback);
}

void FrameViewAsh::ClearToggleResizeLockMenuCallback() {
  toggle_resize_lock_menu_callback_.Reset();
}

void FrameViewAsh::OnWindowPropertyChanged(aura::Window* window,
                                           const void* key,
                                           intptr_t old) {
  // ChromeOS has rounded windows for certain window states. If these states
  // changes, we need to update the rounded corners of the frame associate with
  // the `window`accordingly.
  if (key == chromeos::kWindowHasRoundedCornersKey) {
    UpdateWindowRoundedCorners();

    // For overlapped frames header_view_ layer needs to non-opaque to avoid
    // visual artifacts at the upper corners.
    // See comment in FrameViewAsh::SetFrameOverlapped.
    bool fills_bounds_opaquely = true;
    if (frame_overlapped_ &&
        WindowState::Get(window)->ShouldWindowHaveRoundedCorners()) {
      fills_bounds_opaquely = false;
    }
    if (header_view_->layer()) {
      header_view_->layer()->SetFillsBoundsOpaquely(fills_bounds_opaquely);
    }
  }
}

void FrameViewAsh::OnWindowDestroying(aura::Window* window) {
  window_observation_.Reset();
}

void FrameViewAsh::UpdateWindowRoundedCorners() {
  if (!GetWidget()) {
    return;
  }

  aura::Window* window = GetWidget()->GetNativeWindow();
  auto* window_state = ash::WindowState::Get(window);

  // For certain windows, we do not window state associated with them. (See
  // `ash::WindowState::Get()` for details)
  if (!window_state) {
    return;
  }

  const gfx::RoundedCornersF window_radii =
      window_state->GetWindowRoundedCorners();

  if (frame_enabled_) {
    CHECK_EQ(window_radii.upper_left(), window_radii.upper_right());
    header_view_->SetHeaderCornerRadius(window_radii.upper_left());
  }

  GetWidget()->client_view()->UpdateWindowRoundedCorners(window_radii);
}

base::RepeatingCallback<void()> FrameViewAsh::GetToggleResizeLockMenuCallback()
    const {
  return toggle_resize_lock_menu_callback_;
}

void FrameViewAsh::OnDidSchedulePaint(const gfx::Rect& r) {
  // We may end up here before |header_view_| has been added to the Widget.
  if (header_view_->GetWidget()) {
    // The HeaderView is not a child of FrameViewAsh. Redirect the
    // paint to HeaderView instead.
    gfx::RectF to_paint(r);
    views::View::ConvertRectToTarget(this, GetHeaderView(), &to_paint);
    header_view_->SchedulePaintInRect(gfx::ToEnclosingRect(to_paint));
  }
}

void FrameViewAsh::AddedToWidget() {
  if (highlight_border_overlay_ ||
      !GetWidget()->GetNativeWindow()->GetProperty(
          chromeos::kShouldHaveHighlightBorderOverlay)) {
    return;
  }

  highlight_border_overlay_ = std::make_unique<HighlightBorderOverlay>(
      GetWidget(), std::make_unique<ash::WmHighlightBorderOverlayDelegate>());
}

chromeos::FrameCaptionButtonContainerView*
FrameViewAsh::GetFrameCaptionButtonContainerViewForTest() {
  return header_view_->caption_button_container();
}

void FrameViewAsh::UpdateDefaultFrameColors() {
  aura::Window* frame_window = widget_->GetNativeWindow();
  if (!frame_window->GetProperty(kTrackDefaultFrameColors))
    return;

  auto* color_provider = widget_->GetColorProvider();
  const SkColor dialog_title_bar_color =
      color_provider->GetColor(cros_tokens::kDialogTitleBarColor);

  frame_window->SetProperty(kFrameActiveColorKey, dialog_title_bar_color);
  frame_window->SetProperty(kFrameInactiveColorKey, dialog_title_bar_color);
}

BEGIN_METADATA(FrameViewAsh)
END_METADATA

}  // namespace ash