blob: 15513fd1cc35deb9a54a7241c0e2d0084c6501ae [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/exo/shell_surface.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_state_type.h"
#include "ash/shell.h"
#include "ash/wm/toplevel_window_event_handler.h"
#include "ash/wm/window_resizer.h"
#include "ash/wm/window_state.h"
#include "base/bind.h"
#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/wm_helper.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/env.h"
#include "ui/aura/window.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_tree_host.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/window_util.h"
namespace exo {
namespace {
// Maximum amount of time to wait for contents after a change to maximize,
// fullscreen or pinned state.
constexpr int kMaximizedOrFullscreenOrPinnedLockTimeoutMs = 100;
} // namespace
////////////////////////////////////////////////////////////////////////////////
// ShellSurface, ScopedAnimationsDisabled:
// Helper class used to temporarily disable animations. Restores the
// animations disabled property when instance is destroyed.
class ShellSurface::ScopedAnimationsDisabled {
public:
explicit ScopedAnimationsDisabled(ShellSurface* shell_surface);
~ScopedAnimationsDisabled();
private:
ShellSurface* const shell_surface_;
bool saved_animations_disabled_ = false;
DISALLOW_COPY_AND_ASSIGN(ScopedAnimationsDisabled);
};
ShellSurface::ScopedAnimationsDisabled::ScopedAnimationsDisabled(
ShellSurface* shell_surface)
: shell_surface_(shell_surface) {
if (shell_surface_->widget_) {
aura::Window* window = shell_surface_->widget_->GetNativeWindow();
saved_animations_disabled_ =
window->GetProperty(aura::client::kAnimationsDisabledKey);
window->SetProperty(aura::client::kAnimationsDisabledKey, true);
}
}
ShellSurface::ScopedAnimationsDisabled::~ScopedAnimationsDisabled() {
if (shell_surface_->widget_) {
aura::Window* window = shell_surface_->widget_->GetNativeWindow();
DCHECK_EQ(window->GetProperty(aura::client::kAnimationsDisabledKey), true);
window->SetProperty(aura::client::kAnimationsDisabledKey,
saved_animations_disabled_);
}
}
////////////////////////////////////////////////////////////////////////////////
// ShellSurface, Config:
// Surface state associated with each configure request.
struct ShellSurface::Config {
Config(uint32_t serial,
const gfx::Vector2d& origin_offset,
int resize_component,
std::unique_ptr<ui::CompositorLock> compositor_lock);
~Config() = default;
uint32_t serial;
gfx::Vector2d origin_offset;
int resize_component;
std::unique_ptr<ui::CompositorLock> compositor_lock;
};
ShellSurface::Config::Config(
uint32_t serial,
const gfx::Vector2d& origin_offset,
int resize_component,
std::unique_ptr<ui::CompositorLock> compositor_lock)
: serial(serial),
origin_offset(origin_offset),
resize_component(resize_component),
compositor_lock(std::move(compositor_lock)) {}
////////////////////////////////////////////////////////////////////////////////
// ShellSurface, ScopedConfigure:
ShellSurface::ScopedConfigure::ScopedConfigure(ShellSurface* shell_surface,
bool force_configure)
: shell_surface_(shell_surface), force_configure_(force_configure) {
// ScopedConfigure instances cannot be nested.
DCHECK(!shell_surface_->scoped_configure_);
shell_surface_->scoped_configure_ = this;
}
ShellSurface::ScopedConfigure::~ScopedConfigure() {
DCHECK_EQ(shell_surface_->scoped_configure_, this);
shell_surface_->scoped_configure_ = nullptr;
if (needs_configure_ || force_configure_)
shell_surface_->Configure();
// ScopedConfigure instance might have suppressed a widget bounds update.
if (shell_surface_->widget_) {
shell_surface_->UpdateWidgetBounds();
shell_surface_->UpdateShadow();
}
}
////////////////////////////////////////////////////////////////////////////////
// ShellSurface, public:
ShellSurface::ShellSurface(Surface* surface,
const gfx::Point& origin,
bool activatable,
bool can_minimize,
int container)
: ShellSurfaceBase(surface, origin, activatable, can_minimize, container) {}
ShellSurface::ShellSurface(Surface* surface)
: ShellSurfaceBase(surface,
gfx::Point(),
true,
true,
ash::kShellWindowId_DefaultContainer) {}
ShellSurface::~ShellSurface() {
DCHECK(!scoped_configure_);
if (widget_)
ash::wm::GetWindowState(widget_->GetNativeWindow())->RemoveObserver(this);
}
void ShellSurface::AcknowledgeConfigure(uint32_t serial) {
TRACE_EVENT1("exo", "ShellSurface::AcknowledgeConfigure", "serial", serial);
// Apply all configs that are older or equal to |serial|. The result is that
// the origin of the main surface will move and the resize direction will
// change to reflect the acknowledgement of configure request with |serial|
// at the next call to Commit().
while (!pending_configs_.empty()) {
std::unique_ptr<Config> config = std::move(pending_configs_.front());
pending_configs_.pop_front();
// Add the config offset to the accumulated offset that will be applied when
// Commit() is called.
pending_origin_offset_ += config->origin_offset;
// Set the resize direction that will be applied when Commit() is called.
pending_resize_component_ = config->resize_component;
if (config->serial == serial)
break;
}
if (widget_) {
UpdateWidgetBounds();
UpdateShadow();
}
}
void ShellSurface::SetParent(ShellSurface* parent) {
TRACE_EVENT1("exo", "ShellSurface::SetParent", "parent",
parent ? base::UTF16ToASCII(parent->title_) : "null");
SetParentWindow(parent ? parent->GetWidget()->GetNativeWindow() : nullptr);
}
void ShellSurface::Maximize() {
TRACE_EVENT0("exo", "ShellSurface::Maximize");
if (!widget_) {
initial_show_state_ = ui::SHOW_STATE_MAXIMIZED;
return;
}
// Note: This will ask client to configure its surface even if already
// maximized.
ScopedConfigure scoped_configure(this, true);
widget_->Maximize();
}
void ShellSurface::Minimize() {
TRACE_EVENT0("exo", "ShellSurface::Minimize");
if (!widget_) {
initial_show_state_ = ui::SHOW_STATE_MINIMIZED;
return;
}
// Note: This will ask client to configure its surface even if already
// minimized.
ScopedConfigure scoped_configure(this, true);
widget_->Minimize();
}
void ShellSurface::Restore() {
TRACE_EVENT0("exo", "ShellSurface::Restore");
if (!widget_) {
initial_show_state_ = ui::SHOW_STATE_NORMAL;
return;
}
// Note: This will ask client to configure its surface even if not already
// maximized or minimized.
ScopedConfigure scoped_configure(this, true);
widget_->Restore();
}
void ShellSurface::SetFullscreen(bool fullscreen) {
TRACE_EVENT1("exo", "ShellSurface::SetFullscreen", "fullscreen", fullscreen);
if (!widget_) {
initial_show_state_ = ui::SHOW_STATE_FULLSCREEN;
return;
}
// Note: This will ask client to configure its surface even if fullscreen
// state doesn't change.
ScopedConfigure scoped_configure(this, true);
widget_->SetFullscreen(fullscreen);
}
void ShellSurface::SetPopup() {
DCHECK(!widget_);
is_popup_ = true;
}
void ShellSurface::Grab() {
DCHECK(is_popup_);
DCHECK(!widget_);
has_grab_ = true;
}
void ShellSurface::StartMove() {
TRACE_EVENT0("exo", "ShellSurface::StartMove");
if (!widget_)
return;
AttemptToStartDrag(HTCAPTION);
}
void ShellSurface::StartResize(int component) {
TRACE_EVENT1("exo", "ShellSurface::StartResize", "component", component);
if (!widget_)
return;
AttemptToStartDrag(component);
}
////////////////////////////////////////////////////////////////////////////////
// SurfaceDelegate overrides:
void ShellSurface::OnSetParent(Surface* parent, const gfx::Point& position) {
views::Widget* parent_widget =
parent ? views::Widget::GetTopLevelWidgetForNativeView(parent->window())
: nullptr;
if (parent_widget) {
// Set parent window if using default container and the container itself
// is not the parent.
if (container_ == ash::kShellWindowId_DefaultContainer)
SetParentWindow(parent_widget->GetNativeWindow());
origin_ = position;
views::View::ConvertPointToScreen(
parent_widget->widget_delegate()->GetContentsView(), &origin_);
if (!widget_)
return;
ash::wm::WindowState* window_state =
ash::wm::GetWindowState(widget_->GetNativeWindow());
if (window_state->is_dragged())
return;
gfx::Rect widget_bounds = widget_->GetWindowBoundsInScreen();
gfx::Rect new_widget_bounds(origin_, widget_bounds.size());
if (new_widget_bounds != widget_bounds) {
base::AutoReset<bool> auto_ignore_window_bounds_changes(
&ignore_window_bounds_changes_, true);
widget_->SetBounds(new_widget_bounds);
UpdateSurfaceBounds();
}
} else {
SetParentWindow(nullptr);
}
}
////////////////////////////////////////////////////////////////////////////////
// ShellSurfaceBase overrides:
void ShellSurface::InitializeWindowState(ash::wm::WindowState* window_state) {
window_state->AddObserver(this);
// Sommelier sets the null application id for override redirect windows,
// which controls its bounds by itself.
bool emulate_x11_override_redirect =
(GetShellApplicationId(window_state->window()) == nullptr) && !!parent_;
window_state->set_allow_set_bounds_direct(emulate_x11_override_redirect);
widget_->set_movement_disabled(movement_disabled_);
window_state->set_ignore_keyboard_bounds_change(movement_disabled_);
}
base::Optional<gfx::Rect> ShellSurface::GetWidgetBounds() const {
// Defer if configure requests are pending.
if (!pending_configs_.empty() || scoped_configure_)
return base::nullopt;
gfx::Rect visible_bounds = GetVisibleBounds();
gfx::Rect new_widget_bounds =
widget_->non_client_view()
? widget_->non_client_view()->GetWindowBoundsForClientBounds(
visible_bounds)
: visible_bounds;
if (movement_disabled_) {
new_widget_bounds.set_origin(origin_);
} else if (resize_component_ == HTCAPTION) {
// Preserve widget position.
new_widget_bounds.set_origin(widget_->GetWindowBoundsInScreen().origin());
} else {
// Compute widget origin using surface origin if the current location of
// surface is being anchored to one side of the widget as a result of a
// resize operation.
gfx::Rect visible_bounds = GetVisibleBounds();
gfx::Point origin = GetSurfaceOrigin() + visible_bounds.OffsetFromOrigin();
wm::ConvertPointToScreen(widget_->GetNativeWindow(), &origin);
new_widget_bounds.set_origin(origin);
}
return new_widget_bounds;
}
gfx::Point ShellSurface::GetSurfaceOrigin() const {
DCHECK(!movement_disabled_ || resize_component_ == HTCAPTION);
gfx::Rect visible_bounds = GetVisibleBounds();
gfx::Rect client_bounds = GetClientViewBounds();
switch (resize_component_) {
case HTCAPTION:
return gfx::Point() + origin_offset_ - visible_bounds.OffsetFromOrigin();
case HTBOTTOM:
case HTRIGHT:
case HTBOTTOMRIGHT:
return gfx::Point() - visible_bounds.OffsetFromOrigin();
case HTTOP:
case HTTOPRIGHT:
return gfx::Point(0, client_bounds.height() - visible_bounds.height()) -
visible_bounds.OffsetFromOrigin();
case HTLEFT:
case HTBOTTOMLEFT:
return gfx::Point(client_bounds.width() - visible_bounds.width(), 0) -
visible_bounds.OffsetFromOrigin();
case HTTOPLEFT:
return gfx::Point(client_bounds.width() - visible_bounds.width(),
client_bounds.height() - visible_bounds.height()) -
visible_bounds.OffsetFromOrigin();
default:
NOTREACHED();
return gfx::Point();
}
}
////////////////////////////////////////////////////////////////////////////////
// aura::WindowObserver overrides:
void ShellSurface::OnWindowBoundsChanged(aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
if (!widget_ || !root_surface() || ignore_window_bounds_changes_)
return;
if (window == widget_->GetNativeWindow()) {
if (new_bounds.size() == old_bounds.size())
return;
// If size changed then give the client a chance to produce new contents
// before origin on screen is changed. Retain the old origin by reverting
// the origin delta until the next configure is acknowledged.
gfx::Vector2d delta = new_bounds.origin() - old_bounds.origin();
origin_offset_ -= delta;
pending_origin_offset_accumulator_ += delta;
UpdateSurfaceBounds();
// The shadow size may be updated to match the widget. Change it back
// to the shadow content size. Note that this relies on wm::ShadowController
// being notified of the change before |this|.
UpdateShadow();
Configure();
}
}
////////////////////////////////////////////////////////////////////////////////
// ash::wm::WindowStateObserver overrides:
void ShellSurface::OnPreWindowStateTypeChange(
ash::wm::WindowState* window_state,
ash::mojom::WindowStateType old_type) {
ash::mojom::WindowStateType new_type = window_state->GetStateType();
if (ash::IsMinimizedWindowStateType(old_type) ||
ash::IsMinimizedWindowStateType(new_type)) {
return;
}
if (ash::IsMaximizedOrFullscreenOrPinnedWindowStateType(old_type) ||
ash::IsMaximizedOrFullscreenOrPinnedWindowStateType(new_type)) {
if (!widget_)
return;
// When transitioning in/out of maximized or fullscreen mode, we need to
// make sure we have a configure callback before we allow the default
// cross-fade animations. The configure callback provides a mechanism for
// the client to inform us that a frame has taken the state change into
// account, and without this cross-fade animations are unreliable.
if (!configure_callback_.is_null()) {
// Give client a chance to produce a frame that takes state change into
// account by acquiring a compositor lock.
ui::Compositor* compositor =
widget_->GetNativeWindow()->layer()->GetCompositor();
configure_compositor_lock_ = compositor->GetCompositorLock(
nullptr, base::TimeDelta::FromMilliseconds(
kMaximizedOrFullscreenOrPinnedLockTimeoutMs));
} else {
scoped_animations_disabled_ =
std::make_unique<ScopedAnimationsDisabled>(this);
}
}
}
void ShellSurface::OnPostWindowStateTypeChange(
ash::wm::WindowState* window_state,
ash::mojom::WindowStateType old_type) {
ash::mojom::WindowStateType new_type = window_state->GetStateType();
if (ash::IsMaximizedOrFullscreenOrPinnedWindowStateType(new_type)) {
Configure();
}
if (widget_) {
UpdateWidgetBounds();
UpdateShadow();
}
// Re-enable animations if they were disabled in pre state change handler.
scoped_animations_disabled_.reset();
}
////////////////////////////////////////////////////////////////////////////////
// ShellSurfaceBase overrides:
void ShellSurface::SetWidgetBounds(const gfx::Rect& bounds) {
if (bounds == widget_->GetWindowBoundsInScreen())
return;
// Set |ignore_window_bounds_changes_| as this change to window bounds
// should not result in a configure request.
DCHECK(!ignore_window_bounds_changes_);
ignore_window_bounds_changes_ = true;
widget_->SetBounds(bounds);
UpdateSurfaceBounds();
ignore_window_bounds_changes_ = false;
}
bool ShellSurface::OnPreWidgetCommit() {
if (!widget_ && enabled()) {
// Defer widget creation and commit until surface has contents.
if (host_window()->bounds().IsEmpty()) {
Configure();
return false;
}
CreateShellSurfaceWidget(initial_show_state_);
}
// Apply the accumulated pending origin offset to reflect acknowledged
// configure requests.
origin_offset_ += pending_origin_offset_;
pending_origin_offset_ = gfx::Vector2d();
// Update resize direction to reflect acknowledged configure requests.
resize_component_ = pending_resize_component_;
return true;
}
void ShellSurface::OnPostWidgetCommit() {}
////////////////////////////////////////////////////////////////////////////////
// wm::ActivationChangeObserver overrides:
void ShellSurface::OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
ShellSurfaceBase::OnWindowActivated(reason, gained_active, lost_active);
if (!widget_)
return;
if (gained_active == widget_->GetNativeWindow() ||
lost_active == widget_->GetNativeWindow()) {
Configure();
}
}
////////////////////////////////////////////////////////////////////////////////
// ShellSurface, private:
void ShellSurface::Configure() {
// Delay configure callback if |scoped_configure_| is set.
if (scoped_configure_) {
scoped_configure_->set_needs_configure();
return;
}
gfx::Vector2d origin_offset = pending_origin_offset_accumulator_;
pending_origin_offset_accumulator_ = gfx::Vector2d();
int resize_component = HTCAPTION;
if (widget_) {
ash::wm::WindowState* window_state =
ash::wm::GetWindowState(widget_->GetNativeWindow());
// If surface is being resized, save the resize direction.
if (window_state->is_dragged())
resize_component = window_state->drag_details()->window_component;
}
uint32_t serial = 0;
if (!configure_callback_.is_null()) {
if (widget_) {
serial = configure_callback_.Run(
GetClientViewBounds().size(),
ash::wm::GetWindowState(widget_->GetNativeWindow())->GetStateType(),
IsResizing(), widget_->IsActive(), origin_offset);
} else {
serial = configure_callback_.Run(gfx::Size(),
ash::mojom::WindowStateType::NORMAL,
false, false, origin_offset);
}
}
if (!serial) {
pending_origin_offset_ += origin_offset;
pending_resize_component_ = resize_component;
return;
}
// Apply origin offset and resize component at the first Commit() after this
// configure request has been acknowledged.
pending_configs_.push_back(
std::make_unique<Config>(serial, origin_offset, resize_component,
std::move(configure_compositor_lock_)));
LOG_IF(WARNING, pending_configs_.size() > 100)
<< "Number of pending configure acks for shell surface has reached: "
<< pending_configs_.size();
}
void ShellSurface::AttemptToStartDrag(int component) {
ash::wm::WindowState* window_state =
ash::wm::GetWindowState(widget_->GetNativeWindow());
// Ignore if surface is already being dragged.
if (window_state->is_dragged())
return;
aura::Window* target = widget_->GetNativeWindow();
ash::ToplevelWindowEventHandler* toplevel_handler =
ash::Shell::Get()->toplevel_window_event_handler();
aura::Window* mouse_pressed_handler =
target->GetHost()->dispatcher()->mouse_pressed_handler();
// Start dragging only if:
// 1) touch guesture is in progress.
// 2) mouse was pressed on the target or its subsurfaces.
aura::Window* gesture_target = toplevel_handler->gesture_target();
if (!gesture_target && !mouse_pressed_handler &&
target->Contains(mouse_pressed_handler)) {
return;
}
auto end_drag = [](ShellSurface* shell_surface,
ash::wm::WmToplevelWindowEventHandler::DragResult result) {
shell_surface->EndDrag();
};
if (gesture_target) {
gfx::Point location = toplevel_handler->event_location_in_gesture_target();
aura::Window::ConvertPointToTarget(
gesture_target, widget_->GetNativeWindow()->GetRootWindow(), &location);
toplevel_handler->AttemptToStartDrag(
target, location, component,
base::BindOnce(end_drag, base::Unretained(this)));
} else {
gfx::Point location = WMHelper::GetInstance()->env()->last_mouse_location();
::wm::ConvertPointFromScreen(widget_->GetNativeWindow()->GetRootWindow(),
&location);
toplevel_handler->AttemptToStartDrag(
target, location, component,
base::BindOnce(end_drag, base::Unretained(this)));
}
// Notify client that resizing state has changed.
if (IsResizing())
Configure();
}
void ShellSurface::EndDrag() {
if (resize_component_ != HTCAPTION) {
// Clear the drag details here as Configure uses it to decide if
// the window is being dragged.
ash::wm::GetWindowState(widget_->GetNativeWindow())->DeleteDragDetails();
Configure();
}
}
} // namespace exo