// Copyright 2018 The Chromium Authors // 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 #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(targeter); target = target->GetParentTarget(); } while (target); return nullptr; } } // namespace void SetShellApplicationId(ui::PropertyHandler* property_handler, const absl::optional& 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& 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& 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 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& 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(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(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(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