| // 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/non_client_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 "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/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/header_view.h" |
| #include "chromeos/ui/frame/immersive/immersive_fullscreen_controller.h" |
| #include "chromeos/ui/frame/non_client_frame_view_base.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/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::NonClientFrameViewAsh*) |
| |
| 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(NonClientFrameViewAsh*, |
| kNonClientFrameViewAshKey, |
| 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 NonClientFrameViewAshImmersiveHelper : public WindowStateObserver, |
| public aura::WindowObserver, |
| public display::DisplayObserver { |
| public: |
| NonClientFrameViewAshImmersiveHelper(views::Widget* widget, |
| NonClientFrameViewAsh* 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()); |
| } |
| NonClientFrameViewAshImmersiveHelper( |
| const NonClientFrameViewAshImmersiveHelper&) = delete; |
| NonClientFrameViewAshImmersiveHelper& operator=( |
| const NonClientFrameViewAshImmersiveHelper&) = delete; |
| |
| ~NonClientFrameViewAshImmersiveHelper() 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}; |
| }; |
| |
| NonClientFrameViewAsh::NonClientFrameViewAsh(views::Widget* frame) |
| : chromeos::NonClientFrameViewBase(frame), |
| frame_context_menu_controller_( |
| std::make_unique<FrameContextMenuController>(frame, this)) { |
| header_view_->set_immersive_mode_changed_callback(base::BindRepeating( |
| &NonClientFrameViewAsh::InvalidateLayout, weak_factory_.GetWeakPtr())); |
| |
| aura::Window* frame_window = frame->GetNativeWindow(); |
| window_util::InstallResizeHandleWindowTargeterForWindow(frame_window); |
| |
| // A delegate may be set which takes over the responsibilities of the |
| // NonClientFrameViewAshImmersiveHelper. 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<NonClientFrameViewAshImmersiveHelper>(frame, this); |
| } |
| |
| frame_window->SetProperty(kNonClientFrameViewAshKey, this); |
| window_observation_.Observe(frame_window); |
| |
| header_view_->set_context_menu_controller( |
| frame_context_menu_controller_.get()); |
| } |
| |
| NonClientFrameViewAsh::~NonClientFrameViewAsh() { |
| header_view_->set_context_menu_controller(nullptr); |
| } |
| |
| // static |
| NonClientFrameViewAsh* NonClientFrameViewAsh::Get(aura::Window* window) { |
| return window->GetProperty(kNonClientFrameViewAshKey); |
| } |
| |
| void NonClientFrameViewAsh::InitImmersiveFullscreenControllerForView( |
| ImmersiveFullscreenController* immersive_fullscreen_controller) { |
| immersive_fullscreen_controller->Init(GetHeaderView(), frame_, |
| GetHeaderView()); |
| } |
| |
| void NonClientFrameViewAsh::SetFrameColors(SkColor active_frame_color, |
| SkColor inactive_frame_color) { |
| aura::Window* frame_window = frame_->GetNativeWindow(); |
| frame_window->SetProperty(kTrackDefaultFrameColors, false); |
| frame_window->SetProperty(kFrameActiveColorKey, active_frame_color); |
| frame_window->SetProperty(kFrameInactiveColorKey, inactive_frame_color); |
| } |
| |
| void NonClientFrameViewAsh::SetCaptionButtonModel( |
| std::unique_ptr<chromeos::CaptionButtonModel> model) { |
| header_view_->caption_button_container()->SetModel(std::move(model)); |
| header_view_->UpdateCaptionButtons(); |
| } |
| |
| gfx::Rect NonClientFrameViewAsh::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 NonClientFrameViewAsh::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 NonClientFrameViewAsh::SetShouldPaintHeader(bool paint) { |
| header_view_->SetShouldPaintHeader(paint); |
| } |
| |
| int NonClientFrameViewAsh::NonClientTopBorderPreferredHeight() const { |
| return header_view_->GetPreferredHeight(); |
| } |
| |
| const views::View* NonClientFrameViewAsh::GetAvatarIconViewForTest() const { |
| return header_view_->avatar_icon(); |
| } |
| |
| SkColor NonClientFrameViewAsh::GetActiveFrameColorForTest() const { |
| return frame_->GetNativeWindow()->GetProperty(kFrameActiveColorKey); |
| } |
| |
| SkColor NonClientFrameViewAsh::GetInactiveFrameColorForTest() const { |
| return frame_->GetNativeWindow()->GetProperty(kFrameInactiveColorKey); |
| } |
| |
| void NonClientFrameViewAsh::SetFrameEnabled(bool enabled) { |
| if (enabled == frame_enabled_) |
| return; |
| |
| frame_enabled_ = enabled; |
| overlay_view_->SetVisible(frame_enabled_); |
| UpdateWindowRoundedCorners(); |
| InvalidateLayout(); |
| } |
| |
| void NonClientFrameViewAsh::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. |
| if (chromeos::ShouldWindowHaveRoundedCorners(frame_->GetNativeWindow())) { |
| fills_bounds_opaquely = false; |
| } |
| } |
| if (header_view_->layer()) { |
| header_view_->layer()->SetFillsBoundsOpaquely(fills_bounds_opaquely); |
| } |
| |
| frame_overlapped_ = overlapped; |
| InvalidateLayout(); |
| } |
| |
| void NonClientFrameViewAsh::SetToggleResizeLockMenuCallback( |
| base::RepeatingCallback<void()> callback) { |
| toggle_resize_lock_menu_callback_ = std::move(callback); |
| } |
| |
| void NonClientFrameViewAsh::ClearToggleResizeLockMenuCallback() { |
| toggle_resize_lock_menu_callback_.Reset(); |
| } |
| |
| void NonClientFrameViewAsh::OnWindowPropertyChanged(aura::Window* window, |
| const void* key, |
| intptr_t old) { |
| // ChromeOS has rounded frames for certain window states. If these states |
| // changes, we need to update the rounded corners of the frame associate with |
| // the `window`accordingly. |
| if (chromeos::CanPropertyEffectFrameRadius(key)) { |
| UpdateWindowRoundedCorners(); |
| |
| bool fills_bounds_opaquely = true; |
| // For overlapped frames header_view_ layer needs to non-opaque to avoid |
| // visual artifacts at the upper corners. |
| // See comment in NonClientFrameViewAsh::SetFrameOverlapped. |
| if (frame_overlapped_ && |
| chromeos::ShouldWindowHaveRoundedCorners(frame_->GetNativeWindow())) { |
| fills_bounds_opaquely = false; |
| } |
| if (header_view_->layer()) { |
| header_view_->layer()->SetFillsBoundsOpaquely(fills_bounds_opaquely); |
| } |
| } |
| } |
| |
| void NonClientFrameViewAsh::OnWindowDestroying(aura::Window* window) { |
| window_observation_.Reset(); |
| } |
| |
| void NonClientFrameViewAsh::UpdateWindowRoundedCorners() { |
| if (!GetWidget()) { |
| return; |
| } |
| |
| aura::Window* frame_window = GetWidget()->GetNativeWindow(); |
| |
| const int corner_radius = chromeos::GetFrameCornerRadius(frame_window); |
| frame_window->SetProperty(aura::client::kWindowCornerRadiusKey, |
| corner_radius); |
| |
| if (frame_enabled_) { |
| header_view_->SetHeaderCornerRadius(corner_radius); |
| } |
| |
| if (!chromeos::features::IsRoundedWindowsEnabled()) { |
| return; |
| } |
| |
| GetWidget()->client_view()->UpdateWindowRoundedCorners(corner_radius); |
| } |
| |
| base::RepeatingCallback<void()> |
| NonClientFrameViewAsh::GetToggleResizeLockMenuCallback() const { |
| return toggle_resize_lock_menu_callback_; |
| } |
| |
| void NonClientFrameViewAsh::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 NonClientFrameViewAsh. 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 NonClientFrameViewAsh::AddedToWidget() { |
| if (highlight_border_overlay_ || |
| !GetWidget()->GetNativeWindow()->GetProperty( |
| chromeos::kShouldHaveHighlightBorderOverlay)) { |
| return; |
| } |
| |
| highlight_border_overlay_ = |
| std::make_unique<HighlightBorderOverlay>(GetWidget()); |
| } |
| |
| chromeos::FrameCaptionButtonContainerView* |
| NonClientFrameViewAsh::GetFrameCaptionButtonContainerViewForTest() { |
| return header_view_->caption_button_container(); |
| } |
| |
| void NonClientFrameViewAsh::UpdateDefaultFrameColors() { |
| aura::Window* frame_window = frame_->GetNativeWindow(); |
| if (!frame_window->GetProperty(kTrackDefaultFrameColors)) |
| return; |
| |
| auto* color_provider = frame_->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(NonClientFrameViewAsh) |
| END_METADATA |
| |
| } // namespace ash |