blob: 3282728e0491a89dde8c175cf171d266448bffdb [file] [log] [blame]
// Copyright 2018 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_util.h"
#include <memory>
#include "base/strings/string_number_conversions.h"
#include "base/trace_event/trace_event.h"
#include "build/chromeos_buildflags.h"
#include "components/exo/permission.h"
#include "components/exo/shell_surface_base.h"
#include "components/exo/surface.h"
#include "components/exo/window_properties.h"
#include "components/exo/wm_helper.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/capture_client.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/aura/window_targeter.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/window_util.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/public/cpp/window_properties.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_util.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/exo/client_controlled_shell_surface.h"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
namespace exo {
namespace {
DEFINE_UI_CLASS_PROPERTY_KEY(Surface*, kRootSurfaceKey, nullptr)
// Startup Id set by the client.
DEFINE_OWNED_UI_CLASS_PROPERTY_KEY(std::string, kStartupIdKey, nullptr)
#if BUILDFLAG(IS_CHROMEOS_ASH)
// A property key containing the client controlled shell surface.
DEFINE_UI_CLASS_PROPERTY_KEY(ClientControlledShellSurface*,
kClientControlledShellSurface,
nullptr)
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
// Returns true if the component for a located event should be taken care of
// by the window system.
bool ShouldHTComponentBlocked(int component) {
switch (component) {
case HTCAPTION:
case HTCLOSE:
case HTMAXBUTTON:
case HTMINBUTTON:
case HTMENU:
case HTSYSMENU:
return true;
default:
return false;
}
}
// Find the lowest targeter in the parent chain.
aura::WindowTargeter* FindTargeter(ui::EventTarget* target) {
do {
ui::EventTargeter* targeter = target->GetEventTargeter();
if (targeter)
return static_cast<aura::WindowTargeter*>(targeter);
target = target->GetParentTarget();
} while (target);
return nullptr;
}
} // namespace
void SetShellApplicationId(ui::PropertyHandler* property_handler,
const absl::optional<std::string>& id) {
TRACE_EVENT1("exo", "SetApplicationId", "application_id", id ? *id : "null");
if (id)
property_handler->SetProperty(kApplicationIdKey, *id);
else
property_handler->ClearProperty(kApplicationIdKey);
}
const std::string* GetShellApplicationId(const aura::Window* property_handler) {
return property_handler->GetProperty(kApplicationIdKey);
}
void SetShellStartupId(ui::PropertyHandler* property_handler,
const absl::optional<std::string>& id) {
TRACE_EVENT1("exo", "SetStartupId", "startup_id", id ? *id : "null");
if (id)
property_handler->SetProperty(kStartupIdKey, *id);
else
property_handler->ClearProperty(kStartupIdKey);
}
const std::string* GetShellStartupId(const aura::Window* window) {
return window->GetProperty(kStartupIdKey);
}
void SetShellUseImmersiveForFullscreen(aura::Window* window, bool value) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
window->SetProperty(chromeos::kImmersiveImpliedByFullscreen, value);
// Ensure the shelf is fully hidden in plain fullscreen, but shown
// (auto-hides based on mouse movement) when in immersive fullscreen.
window->SetProperty(chromeos::kHideShelfWhenFullscreenKey, !value);
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
void SetShellClientAccessibilityId(aura::Window* window,
const absl::optional<int32_t>& id) {
TRACE_EVENT1("exo", "SetClientAccessibilityId", "id",
id ? base::NumberToString(*id) : "null");
if (id)
window->SetProperty(ash::kClientAccessibilityIdKey, *id);
else
window->ClearProperty(ash::kClientAccessibilityIdKey);
}
const absl::optional<int32_t> GetShellClientAccessibilityId(
aura::Window* window) {
auto id = window->GetProperty(ash::kClientAccessibilityIdKey);
if (id < 0)
return absl::nullopt;
else
return id;
}
void SetShellClientControlledShellSurface(
ui::PropertyHandler* property_handler,
const absl::optional<ClientControlledShellSurface*>& shell_surface) {
if (shell_surface)
property_handler->SetProperty(kClientControlledShellSurface,
shell_surface.value());
else
property_handler->ClearProperty(kClientControlledShellSurface);
}
ClientControlledShellSurface* GetShellClientControlledShellSurface(
ui::PropertyHandler* property_handler) {
return property_handler->GetProperty(kClientControlledShellSurface);
}
int GetWindowDeskStateChanged(const aura::Window* window) {
constexpr int kToggleVisibleOnAllWorkspacesValue = -1;
if (ash::desks_util::IsWindowVisibleOnAllWorkspaces(window))
return kToggleVisibleOnAllWorkspacesValue;
int workspace = window->GetProperty(aura::client::kWindowWorkspaceKey);
// If workspace is unassigned, returns the active desk index.
if (workspace == aura::client::kWindowWorkspaceUnassignedWorkspace)
workspace = ash::DesksController::Get()->GetActiveDeskIndex();
return workspace;
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
void SetShellRootSurface(ui::PropertyHandler* property_handler,
Surface* surface) {
property_handler->SetProperty(kRootSurfaceKey, surface);
}
Surface* GetShellRootSurface(const aura::Window* window) {
return window->GetProperty(kRootSurfaceKey);
}
ShellSurfaceBase* GetShellSurfaceBaseForWindow(aura::Window* window) {
// Only windows with a surface can have a shell surface.
if (!GetShellRootSurface(window))
return nullptr;
views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window);
if (!widget)
return nullptr;
ShellSurfaceBase* shell_surface_base =
static_cast<ShellSurfaceBase*>(widget->widget_delegate());
// We can obtain widget from native window, but not |shell_surface_base|.
// This means we are in the process of destroying this surface so we should
// return nullptr.
if (!shell_surface_base || !shell_surface_base->GetWidget())
return nullptr;
return shell_surface_base;
}
Surface* GetTargetSurfaceForLocatedEvent(
const ui::LocatedEvent* original_event) {
aura::Window* window =
WMHelper::GetInstance()->GetCaptureClient()->GetCaptureWindow();
if (!window) {
return Surface::AsSurface(
static_cast<aura::Window*>(original_event->target()));
}
Surface* root_surface = GetShellRootSurface(window);
// Skip if the event is captured by non exo windows.
if (!root_surface) {
auto* widget = views::Widget::GetTopLevelWidgetForNativeView(window);
if (!widget)
return nullptr;
root_surface = GetShellRootSurface(widget->GetNativeWindow());
if (!root_surface)
return nullptr;
ShellSurfaceBase* shell_surface_base =
GetShellSurfaceBaseForWindow(widget->GetNativeWindow());
// Check if it's overlay window.
if (!shell_surface_base->host_window()->Contains(window) &&
shell_surface_base->GetWidget()->GetNativeWindow() != window) {
return nullptr;
}
}
// Create a clone of the event as targeter may update it during the
// search.
auto cloned = original_event->Clone();
ui::LocatedEvent* event = cloned->AsLocatedEvent();
while (true) {
gfx::PointF location_in_target_f = event->location_f();
gfx::Point location_in_target = event->location();
ui::EventTarget* event_target = window;
aura::WindowTargeter* targeter = FindTargeter(event_target);
DCHECK(targeter);
aura::Window* focused =
static_cast<aura::Window*>(targeter->FindTargetForEvent(window, event));
if (focused) {
Surface* surface = Surface::AsSurface(focused);
if (focused != window)
return surface;
else if (surface && surface->HitTest(location_in_target)) {
// If the targeting fallback to the root (first) window, test the
// hit region again.
return surface;
}
}
// If the event falls into the place where the window system should care
// about (i.e. window caption), do not check the transient parent but just
// return nullptr. See b/149517682.
if (window->delegate() &&
ShouldHTComponentBlocked(
window->delegate()->GetNonClientComponent(location_in_target))) {
return nullptr;
}
aura::Window* parent_window = wm::GetTransientParent(window);
if (!parent_window)
return root_surface;
event->set_location_f(location_in_target_f);
event_target->ConvertEventToTarget(parent_window, event);
window = parent_window;
}
}
Surface* GetTargetSurfaceForKeyboardFocus(aura::Window* window) {
if (!window)
return nullptr;
// The keyboard focus should be set to the root surface.
ShellSurfaceBase* shell_surface_base = nullptr;
for (auto* current = window; current && !shell_surface_base;
current = current->parent()) {
shell_surface_base = GetShellSurfaceBaseForWindow(current);
}
// Make sure the |window| is the toplevel or a host window, but not
// another window added to the toplevel.
if (shell_surface_base && !shell_surface_base->HasOverlay() &&
(shell_surface_base->GetWidget()->GetNativeWindow() == window ||
shell_surface_base->host_window()->Contains(window))) {
return shell_surface_base->root_surface();
}
// Fallback to the window's surface if any. This is used for
// notifications.
return Surface::AsSurface(window);
}
void GrantPermissionToActivate(aura::Window* window, base::TimeDelta timeout) {
// Activation is the only permission, so just set the property. The window
// owns the Permission object.
window->SetProperty(
kPermissionKey,
new Permission(Permission::Capability::kActivate, timeout));
}
void GrantPermissionToActivateIndefinitely(aura::Window* window) {
// Activation is the only permission, so just set the property. The window
// owns the Permission object.
window->SetProperty(kPermissionKey,
new Permission(Permission::Capability::kActivate));
}
void RevokePermissionToActivate(aura::Window* window) {
// Activation is the only permission, so just clear the property.
window->ClearProperty(kPermissionKey);
}
bool HasPermissionToActivate(aura::Window* window) {
Permission* permission = window->GetProperty(kPermissionKey);
return permission && permission->Check(Permission::Capability::kActivate);
}
bool ConsumedByIme(aura::Window* window, const ui::KeyEvent& event) {
// Check if IME consumed the event, to avoid it to be doubly processed.
// First let us see whether IME is active and is in text input mode.
views::Widget* widget = views::Widget::GetTopLevelWidgetForNativeView(window);
ui::InputMethod* ime = widget ? widget->GetInputMethod() : nullptr;
if (!ime || ime->GetTextInputType() == ui::TEXT_INPUT_TYPE_NONE ||
ime->GetTextInputType() == ui::TEXT_INPUT_TYPE_NULL) {
return false;
}
// Case 1:
// When IME ate a key event but did not emit character insertion event yet
// (e.g., when it is still showing a candidate list UI to the user,) the
// consumed key event is re-sent after masked |key_code| by VKEY_PROCESSKEY.
if (event.key_code() == ui::VKEY_PROCESSKEY)
return true;
// Except for PROCESSKEY, never discard "key-up" events. A keydown not paired
// by a keyup can trigger a never-ending key repeat in the client, which can
// never be desirable.
if (event.type() == ui::ET_KEY_RELEASED)
return false;
// Case 2:
// When IME ate a key event and generated a single character input, it leaves
// the key event as-is, and in addition calls the active ui::TextInputClient's
// InsertChar() method. (In our case, arc::ArcImeService::InsertChar()).
//
// In Chrome OS (and Web) convention, the two calls won't cause duplicates,
// because key-down events do not mean any character inputs there.
// (InsertChar issues a DOM "keypress" event, which is distinct from keydown.)
// Unfortunately, this is not necessary the case for our clients that may
// treat keydown as a trigger of text inputs. We need suppression for keydown.
//
// Same condition as ash/components/arc/ime/arc_ime_service.cc#InsertChar.
const char16_t ch = event.GetCharacter();
const bool is_control_char =
(0x00 <= ch && ch <= 0x1f) || (0x7f <= ch && ch <= 0x9f);
if (!is_control_char && !ui::IsSystemKeyModifier(event.flags()))
return true;
// Case 3:
// Workaround for apps that doesn't handle hardware keyboard events well.
// Keys typically on software keyboard and lack of them are fatal, namely,
// unmodified enter and backspace keys, are sent through IME.
constexpr int kModifierMask = ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN |
ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN |
ui::EF_ALTGR_DOWN | ui::EF_MOD3_DOWN;
// Same condition as ash/components/arc/ime/arc_ime_service.cc#InsertChar.
if ((event.flags() & kModifierMask) == 0) {
if (event.key_code() == ui::VKEY_RETURN ||
event.key_code() == ui::VKEY_BACK) {
return true;
}
}
return false;
}
void SetSkipImeProcessingToDescendentSurfaces(aura::Window* window,
bool value) {
if (Surface::AsSurface(window))
window->SetProperty(aura::client::kSkipImeProcessing, value);
for (aura::Window* child : window->children())
SetSkipImeProcessingToDescendentSurfaces(child, value);
}
} // namespace exo