blob: d199808070d6ffab242a148f6dd8a98d8c925df1 [file] [log] [blame]
// 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 "ui/views/corewm/tooltip_controller.h"
#include <stddef.h>
#include <utility>
#include <vector>
#include "base/time/time.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/capture_client.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/client/drag_drop_client.h"
#include "ui/aura/client/screen_position_client.h"
#include "ui/aura/env.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_type.h"
#include "ui/display/screen.h"
#include "ui/events/event.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/ozone/public/ozone_platform.h"
#include "ui/views/corewm/tooltip_state_manager.h"
#include "ui/views/widget/tooltip_manager.h"
#include "ui/wm/public/activation_client.h"
#include "ui/wm/public/tooltip_observer.h"
namespace views::corewm {
namespace {
constexpr auto kDefaultShowTooltipDelay = base::Milliseconds(500);
constexpr auto kDefaultHideTooltipDelay = base::Seconds(10);
// Returns true if |target| is a valid window to get the tooltip from.
// |event_target| is the original target from the event and |target| the window
// at the same location.
bool IsValidTarget(aura::Window* event_target, aura::Window* target) {
if (!target || (event_target == target))
return true;
// If `target` is contained in `event_target`, it's valid.
// This case may happen on exo surfaces.
if (event_target->Contains(target) &&
event_target->GetBoundsInScreen().Contains(target->GetBoundsInScreen())) {
return true;
}
void* event_target_grouping_id = event_target->GetNativeWindowProperty(
TooltipManager::kGroupingPropertyKey);
auto* toplevel_of_target = target->GetToplevelWindow();
// Return true if grouping id is same for `target` and `event_target`.
// Also, check grouping id of target's toplevel window to allow the child
// window under `target`, because the menu window may have a child window.
return event_target_grouping_id &&
(event_target_grouping_id ==
target->GetNativeWindowProperty(
TooltipManager::kGroupingPropertyKey) ||
(toplevel_of_target &&
event_target_grouping_id ==
toplevel_of_target->GetNativeWindowProperty(
TooltipManager::kGroupingPropertyKey)));
}
// Returns the target (the Window tooltip text comes from) based on the event.
// If a Window other than event.target() is returned, |location| is adjusted
// to be in the coordinates of the returned Window.
aura::Window* GetTooltipTarget(const ui::MouseEvent& event,
gfx::Point* location) {
switch (event.type()) {
case ui::ET_MOUSE_CAPTURE_CHANGED:
// On windows we can get a capture changed without an exit. We need to
// reset state when this happens else the tooltip may incorrectly show.
return nullptr;
case ui::ET_MOUSE_EXITED:
return nullptr;
case ui::ET_MOUSE_MOVED:
case ui::ET_MOUSE_DRAGGED: {
aura::Window* event_target = static_cast<aura::Window*>(event.target());
if (!event_target)
return nullptr;
// If a window other than |event_target| has capture, ignore the event.
// This can happen when RootWindow creates events when showing/hiding, or
// the system generates an extra event. We have to check
// GetGlobalCaptureWindow() as Windows does not use a singleton
// CaptureClient.
if (!event_target->HasCapture()) {
aura::Window* root = event_target->GetRootWindow();
if (root) {
aura::client::CaptureClient* capture_client =
aura::client::GetCaptureClient(root);
if (capture_client) {
aura::Window* capture_window =
capture_client->GetGlobalCaptureWindow();
if (capture_window && event_target != capture_window)
return nullptr;
}
}
return event_target;
}
// If |target| has capture all events go to it, even if the mouse is
// really over another window. Find the real window the mouse is over.
const gfx::Point screen_loc = event.target()->GetScreenLocation(event);
display::Screen* screen = display::Screen::GetScreen();
aura::Window* target = screen->GetWindowAtScreenPoint(screen_loc);
if (!target)
return nullptr;
gfx::Point target_loc(screen_loc);
aura::client::GetScreenPositionClient(target->GetRootWindow())
->ConvertPointFromScreen(target, &target_loc);
aura::Window* screen_target = target->GetEventHandlerForPoint(target_loc);
if (!IsValidTarget(event_target, screen_target))
return nullptr;
aura::Window::ConvertPointToTarget(screen_target, target, &target_loc);
*location = target_loc;
return screen_target;
}
default:
NOTREACHED_NORETURN();
}
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// TooltipController public:
TooltipController::TooltipController(std::unique_ptr<Tooltip> tooltip,
wm::ActivationClient* activation_client)
: activation_client_(activation_client),
state_manager_(
std::make_unique<TooltipStateManager>(std::move(tooltip))) {
if (activation_client_)
activation_client_->AddObserver(this);
}
TooltipController::~TooltipController() {
if (observed_window_)
observed_window_->RemoveObserver(this);
if (activation_client_)
activation_client_->RemoveObserver(this);
}
void TooltipController::AddObserver(wm::TooltipObserver* observer) {
state_manager_->AddObserver(observer);
}
void TooltipController::RemoveObserver(wm::TooltipObserver* observer) {
state_manager_->RemoveObserver(observer);
}
int TooltipController::GetMaxWidth(const gfx::Point& location) const {
return state_manager_->GetMaxWidth(location);
}
void TooltipController::UpdateTooltip(aura::Window* target) {
// The |tooltip_parent_window_| is only set when the tooltip is visible or
// its |will_show_tooltip_timer_| is running.
if (target && observed_window_ == target) {
// This is either an update on an already (or about to be) visible tooltip
// or a call to UpdateIfRequired that will potentially trigger a tooltip
// caused by a tooltip text update.
//
// If there's no active tooltip, it's appropriate to assume that the trigger
// is kCursor because a tooltip text update triggered from the keyboard
// would always happen in UpdateTooltipFromKeyboard, not from here.
if (state_manager_->tooltip_parent_window())
UpdateIfRequired(state_manager_->tooltip_trigger());
else if (IsTooltipTextUpdateNeeded())
UpdateIfRequired(TooltipTrigger::kCursor);
}
ResetWindowAtMousePressedIfNeeded(target, /* force_reset */ false);
}
void TooltipController::UpdateTooltipFromKeyboard(const gfx::Rect& bounds,
aura::Window* target) {
UpdateTooltipFromKeyboardWithAnchorPoint(bounds.bottom_center(), target);
}
void TooltipController::UpdateTooltipFromKeyboardWithAnchorPoint(
const gfx::Point& anchor_point,
aura::Window* target) {
anchor_point_ = anchor_point;
SetObservedWindow(target);
// Update the position of the active but not yet visible keyboard triggered
// tooltip, if any.
if (state_manager_->tooltip_parent_window()) {
state_manager_->UpdatePositionIfNeeded(anchor_point_,
TooltipTrigger::kKeyboard);
}
// This function is always only called for keyboard-triggered tooltips.
UpdateIfRequired(TooltipTrigger::kKeyboard);
ResetWindowAtMousePressedIfNeeded(target, /* force_reset */ true);
}
bool TooltipController::IsTooltipSetFromKeyboard(aura::Window* target) {
return target && target == state_manager_->tooltip_parent_window() &&
state_manager_->tooltip_trigger() == TooltipTrigger::kKeyboard;
}
void TooltipController::SetHideTooltipTimeout(aura::Window* target,
base::TimeDelta timeout) {
hide_tooltip_timeout_map_[target] = timeout;
}
void TooltipController::SetTooltipsEnabled(bool enable) {
if (tooltips_enabled_ == enable)
return;
tooltips_enabled_ = enable;
UpdateTooltip(observed_window_);
}
void TooltipController::OnKeyEvent(ui::KeyEvent* event) {
if (event->type() != ui::ET_KEY_PRESSED)
return;
// Always hide a tooltip on a key press. Since this controller is a pre-target
// handler (i.e. the events are received here before the target act on them),
// hiding the tooltip will not cancel any action supposed to show it triggered
// by a key press.
HideAndReset();
}
void TooltipController::OnMouseEvent(ui::MouseEvent* event) {
// Ignore mouse events that coincide with the last touch event.
if (event->location() == last_touch_loc_) {
// If the tooltip is visible, SetObservedWindow will also hide it if needed.
SetObservedWindow(nullptr);
return;
}
switch (event->type()) {
case ui::ET_MOUSE_EXITED:
// TODO(bebeaudr): Keyboard-triggered tooltips that show up right where
// the cursor currently is are hidden as soon as they show up because of
// this event. Handle this case differently to fix the issue.
//
// Whenever a tooltip is closed, an ET_MOUSE_EXITED event is fired, even
// if the cursor is not in the tooltip's window. Make sure that these
// mouse exited events don't interfere with keyboard triggered tooltips by
// returning early.
if (state_manager_->tooltip_parent_window() &&
state_manager_->tooltip_trigger() == TooltipTrigger::kKeyboard) {
return;
}
SetObservedWindow(nullptr);
break;
case ui::ET_MOUSE_CAPTURE_CHANGED:
case ui::ET_MOUSE_MOVED:
case ui::ET_MOUSE_DRAGGED: {
// Synthesized mouse moves shouldn't cause us to show a tooltip. See
// https://crbug.com/1146981.
if (event->IsSynthesized())
break;
last_mouse_loc_ = event->location();
aura::Window* target = nullptr;
// Avoid a call to display::Screen::GetWindowAtScreenPoint() since it can
// be very expensive on X11 in cases when the tooltip is hidden anyway.
if (tooltips_enabled_ && !aura::Env::GetInstance()->IsMouseButtonDown() &&
!IsDragDropInProgress()) {
target = GetTooltipTarget(*event, &last_mouse_loc_);
}
// This needs to be called after the |last_mouse_loc_| is converted to the
// target's screen coordinates.
state_manager_->UpdatePositionIfNeeded(last_mouse_loc_,
TooltipTrigger::kCursor);
SetObservedWindow(target);
is_duplicate_pen_hover_event_ =
IsDuplicatePenHoverEvent(event->pointer_details().pointer_type);
if (state_manager_->IsVisible() ||
(observed_window_ && IsTooltipTextUpdateNeeded())) {
UpdateIfRequired(TooltipTrigger::kCursor);
}
break;
}
case ui::ET_MOUSE_PRESSED:
if ((event->flags() & ui::EF_IS_NON_CLIENT) == 0) {
aura::Window* target = static_cast<aura::Window*>(event->target());
// We don't get a release for non-client areas.
tooltip_window_at_mouse_press_tracker_.RemoveAll();
if (target) {
tooltip_window_at_mouse_press_tracker_.Add(target);
tooltip_text_at_mouse_press_ = wm::GetTooltipText(target);
}
}
state_manager_->HideAndReset();
break;
case ui::ET_MOUSEWHEEL:
// Hide the tooltip for click, release, drag, wheel events.
if (state_manager_->IsVisible())
state_manager_->HideAndReset();
break;
default:
break;
}
}
void TooltipController::OnTouchEvent(ui::TouchEvent* event) {
// Hide the tooltip for touch events.
HideAndReset();
last_touch_loc_ = event->location();
}
void TooltipController::OnCancelMode(ui::CancelModeEvent* event) {
HideAndReset();
}
base::StringPiece TooltipController::GetLogContext() const {
return "TooltipController";
}
void TooltipController::OnCursorVisibilityChanged(bool is_visible) {
if (is_visible && !state_manager_->tooltip_parent_window()) {
// When there's no tooltip and the cursor becomes visible, the cursor might
// already be over an item that should trigger a tooltip. Update it to
// ensure we don't miss this case.
UpdateIfRequired(TooltipTrigger::kCursor);
} else if (!is_visible && state_manager_->tooltip_parent_window() &&
state_manager_->tooltip_trigger() == TooltipTrigger::kCursor) {
// When the cursor is hidden and we have an active tooltip that was
// triggered by the cursor, hide it.
HideAndReset();
}
}
void TooltipController::OnWindowVisibilityChanged(aura::Window* window,
bool visible) {
// If window is not drawn, skip modifying tooltip.
if (!visible && window->layer()->type() != ui::LAYER_NOT_DRAWN)
HideAndReset();
}
void TooltipController::OnWindowDestroying(aura::Window* window) {
// Reset tooltip before `observed_window_` is destructed since Tooltip::Hide
// which is called by HideAndReset() may try to access to the raw_ptr of the
// window.
if (state_manager_->tooltip_parent_window() == window) {
HideAndReset();
}
}
void TooltipController::OnWindowDestroyed(aura::Window* window) {
if (observed_window_ == window) {
RemoveTooltipDelayFromMap(observed_window_);
observed_window_ = nullptr;
}
}
void TooltipController::OnWindowPropertyChanged(aura::Window* window,
const void* key,
intptr_t old) {
if ((key == wm::kTooltipIdKey || key == wm::kTooltipTextKey) &&
wm::GetTooltipText(window) != std::u16string() &&
(IsTooltipTextUpdateNeeded() || IsTooltipIdUpdateNeeded())) {
UpdateIfRequired(state_manager_->tooltip_trigger());
}
}
void TooltipController::OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
// We want to hide tooltips whenever the client is losing user focus.
if (lost_active)
HideAndReset();
}
void TooltipController::SetShowTooltipDelay(aura::Window* target,
base::TimeDelta delay) {
show_tooltip_delay_map_[target] = delay;
}
#if BUILDFLAG(IS_CHROMEOS_LACROS)
void TooltipController::OnTooltipShownOnServer(aura::Window* window,
const std::u16string& text,
const gfx::Rect& bounds) {
state_manager_->OnTooltipShownOnServer(window, text, bounds);
}
void TooltipController::OnTooltipHiddenOnServer() {
state_manager_->OnTooltipHiddenOnServer();
}
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
////////////////////////////////////////////////////////////////////////////////
// TooltipController private:
void TooltipController::HideAndReset() {
state_manager_->HideAndReset();
SetObservedWindow(nullptr);
}
void TooltipController::UpdateIfRequired(TooltipTrigger trigger) {
if (!tooltips_enabled_ || aura::Env::GetInstance()->IsMouseButtonDown() ||
IsDragDropInProgress() ||
(trigger == TooltipTrigger::kCursor && !IsCursorVisible())) {
state_manager_->HideAndReset();
return;
}
// When a user press a mouse button, we want to hide the tooltip and prevent
// the tooltip from showing up again until the cursor moves to another view
// than the one that received the press event.
if (ShouldHideBecauseMouseWasOncePressed()) {
state_manager_->HideAndReset();
return;
}
tooltip_window_at_mouse_press_tracker_.RemoveAll();
if (trigger == TooltipTrigger::kCursor)
anchor_point_ = last_mouse_loc_;
// If this is a duplicate event generated by a hovering stylus or pen, the
// tooltip has already been updated and its timer should not be restarted.
if (is_duplicate_pen_hover_event_) {
return;
}
// If the uniqueness indicator is different from the previously encountered
// one, we should force tooltip update
if (!state_manager_->IsVisible() || IsTooltipTextUpdateNeeded() ||
IsTooltipIdUpdateNeeded()) {
state_manager_->Show(observed_window_, wm::GetTooltipText(observed_window_),
anchor_point_, trigger, GetShowTooltipDelay(),
GetHideTooltipDelay());
}
}
bool TooltipController::IsDragDropInProgress() const {
if (!observed_window_)
return false;
aura::client::DragDropClient* client =
aura::client::GetDragDropClient(observed_window_->GetRootWindow());
return client && client->IsDragDropInProgress();
}
bool TooltipController::IsCursorVisible() const {
if (!observed_window_)
return false;
aura::Window* root = observed_window_->GetRootWindow();
if (!root)
return false;
aura::client::CursorClient* cursor_client =
aura::client::GetCursorClient(root);
// |cursor_client| may be NULL in tests, treat NULL as always visible.
return !cursor_client || cursor_client->IsCursorVisible();
}
base::TimeDelta TooltipController::GetShowTooltipDelay() {
std::map<aura::Window*, base::TimeDelta>::const_iterator it =
show_tooltip_delay_map_.find(observed_window_);
if (it == show_tooltip_delay_map_.end()) {
return skip_show_delay_for_testing_ ? base::TimeDelta()
: kDefaultShowTooltipDelay;
}
return it->second;
}
base::TimeDelta TooltipController::GetHideTooltipDelay() {
std::map<aura::Window*, base::TimeDelta>::const_iterator it =
hide_tooltip_timeout_map_.find(observed_window_);
if (it == hide_tooltip_timeout_map_.end())
return kDefaultHideTooltipDelay;
return it->second;
}
void TooltipController::SetObservedWindow(aura::Window* target) {
if (observed_window_ == target)
return;
// When we are setting the |observed_window_| to nullptr, it is generally
// because the cursor is over a window not owned by Chromium. To prevent a
// tooltip from being shown after the cursor goes to another window not
// managed by us, hide the the tooltip and cancel all timers that would show
// the tooltip.
if (!target && state_manager_->tooltip_parent_window()) {
// Important: We can't call `TooltipController::HideAndReset` or we'd get an
// infinite loop here.
state_manager_->HideAndReset();
}
if (observed_window_)
observed_window_->RemoveObserver(this);
observed_window_ = target;
if (observed_window_)
observed_window_->AddObserver(this);
}
bool TooltipController::IsTooltipIdUpdateNeeded() const {
return state_manager_->tooltip_id() != wm::GetTooltipId(observed_window_);
}
bool TooltipController::IsTooltipTextUpdateNeeded() const {
return state_manager_->tooltip_text() != wm::GetTooltipText(observed_window_);
}
void TooltipController::RemoveTooltipDelayFromMap(aura::Window* window) {
show_tooltip_delay_map_.erase(window);
hide_tooltip_timeout_map_.erase(window);
}
void TooltipController::ResetWindowAtMousePressedIfNeeded(aura::Window* target,
bool force_reset) {
// Reset tooltip_window_at_mouse_press() if the cursor moved within the same
// window but over a region that has different tooltip text. This handles the
// case of clicking on a view, moving within the same window but over a
// different view, then back to the original view.
if (force_reset ||
(tooltip_window_at_mouse_press() &&
target == tooltip_window_at_mouse_press() &&
wm::GetTooltipText(target) != tooltip_text_at_mouse_press_)) {
tooltip_window_at_mouse_press_tracker_.RemoveAll();
}
}
// TODO(bebeaudr): This approach is less than ideal. It looks at the tooltip
// text at the moment the mouse was pressed to determine whether or not we are
// on the same tooltip as before. This cause problems when two elements are next
// to each other and have the same text - unlikely, but an issue nonetheless.
// However, this is currently the nearest we can get since we don't have an
// identifier of the renderer side element that triggered the tooltip. Could we
// pass a renderer element unique id alongside the tooltip text?
bool TooltipController::ShouldHideBecauseMouseWasOncePressed() {
// Skip hiding when tooltip text is empty as no need to hide again.
// This is required since client-side tooltip appears as empty text on server
// side so that the tooltip is overridden by empty text regardless of the
// actual text to show.
// TODO(crbug.com/1383844): Remove or update this special path when tooltip
// identifier is implemented.
if (wm::GetTooltipText(observed_window_).empty())
return false;
return tooltip_window_at_mouse_press() &&
observed_window_ == tooltip_window_at_mouse_press() &&
wm::GetTooltipText(observed_window_) == tooltip_text_at_mouse_press_;
}
bool TooltipController::IsDuplicatePenHoverEvent(
ui::EventPointerType pointer_type) {
if (pointer_type != ui::EventPointerType::kPen || !observed_window_) {
return false;
}
const auto tooltip_text = wm::GetTooltipText(observed_window_);
if (!tooltip_text.empty() && tooltip_text == last_pen_tooltip_text_) {
return true;
}
last_pen_tooltip_text_ = tooltip_text;
return false;
}
} // namespace views::corewm