blob: bb28c54535c7a3ae057c31678d2e175e4667369b [file] [log] [blame]
// Copyright (c) 2012 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 "ui/views/corewm/tooltip_controller.h"
#include <vector>
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "ui/aura/client/capture_client.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/client/screen_position_client.h"
#include "ui/aura/env.h"
#include "ui/aura/window.h"
#include "ui/events/event.h"
#include "ui/gfx/font.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/screen.h"
#include "ui/gfx/text_elider.h"
#include "ui/views/corewm/tooltip.h"
#include "ui/views/widget/tooltip_manager.h"
#include "ui/wm/public/drag_drop_client.h"
namespace views {
namespace corewm {
namespace {
const int kTooltipTimeoutMs = 500;
const int kDefaultTooltipShownTimeoutMs = 10000;
const size_t kMaxTooltipLength = 2048;
// 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;
void* event_target_grouping_id = event_target->GetNativeWindowProperty(
TooltipManager::kGroupingPropertyKey);
void* target_grouping_id = target->GetNativeWindowProperty(
TooltipManager::kGroupingPropertyKey);
return event_target_grouping_id &&
event_target_grouping_id == target_grouping_id;
}
// 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 NULL;
case ui::ET_MOUSE_EXITED:
return NULL;
case ui::ET_MOUSE_MOVED:
case ui::ET_MOUSE_DRAGGED: {
aura::Window* event_target = static_cast<aura::Window*>(event.target());
if (!event_target)
return NULL;
// 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 NULL;
}
}
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.
gfx::Point screen_loc(event.location());
aura::client::GetScreenPositionClient(event_target->GetRootWindow())->
ConvertPointToScreen(event_target, &screen_loc);
gfx::Screen* screen = gfx::Screen::GetScreenFor(event_target);
aura::Window* target = screen->GetWindowAtScreenPoint(screen_loc);
if (!target)
return NULL;
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 NULL;
aura::Window::ConvertPointToTarget(screen_target, target, &target_loc);
*location = target_loc;
return screen_target;
}
default:
NOTREACHED();
break;
}
return NULL;
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// TooltipController public:
TooltipController::TooltipController(scoped_ptr<Tooltip> tooltip)
: tooltip_window_(NULL),
tooltip_id_(NULL),
tooltip_window_at_mouse_press_(NULL),
tooltip_(tooltip.Pass()),
tooltips_enabled_(true) {
tooltip_timer_.Start(FROM_HERE,
base::TimeDelta::FromMilliseconds(kTooltipTimeoutMs),
this, &TooltipController::TooltipTimerFired);
}
TooltipController::~TooltipController() {
if (tooltip_window_)
tooltip_window_->RemoveObserver(this);
}
int TooltipController::GetMaxWidth(const gfx::Point& location,
gfx::NativeView context) const {
return tooltip_->GetMaxWidth(location, context);
}
void TooltipController::UpdateTooltip(aura::Window* target) {
// If tooltip is visible, we may want to hide it. If it is not, we are ok.
if (tooltip_window_ == target && tooltip_->IsVisible())
UpdateIfRequired();
// Reset |tooltip_window_at_mouse_press_| if the moving within the same window
// but over a region that has different tooltip text. By resetting
// |tooltip_window_at_mouse_press_| we ensure the next time the timer fires
// we'll requery for the tooltip text.
// This handles the case of clicking on a view, moving within the same window
// but over a different view, than back to the original.
if (tooltip_window_at_mouse_press_ &&
target == tooltip_window_at_mouse_press_ &&
aura::client::GetTooltipText(target) != tooltip_text_at_mouse_press_) {
tooltip_window_at_mouse_press_ = NULL;
}
// If we had stopped the tooltip timer for some reason, we must restart it if
// there is a change in the tooltip.
if (!tooltip_timer_.IsRunning()) {
if (tooltip_window_ != target || (tooltip_window_ &&
tooltip_text_ != aura::client::GetTooltipText(tooltip_window_))) {
tooltip_timer_.Start(FROM_HERE,
base::TimeDelta::FromMilliseconds(kTooltipTimeoutMs),
this, &TooltipController::TooltipTimerFired);
}
}
}
void TooltipController::SetTooltipShownTimeout(aura::Window* target,
int timeout_in_ms) {
tooltip_shown_timeout_map_[target] = timeout_in_ms;
}
void TooltipController::SetTooltipsEnabled(bool enable) {
if (tooltips_enabled_ == enable)
return;
tooltips_enabled_ = enable;
UpdateTooltip(tooltip_window_);
}
void TooltipController::OnKeyEvent(ui::KeyEvent* event) {
// On key press, we want to hide the tooltip and not show it until change.
// This is the same behavior as hiding tooltips on timeout. Hence, we can
// simply simulate a timeout.
if (tooltip_shown_timer_.IsRunning()) {
tooltip_shown_timer_.Stop();
TooltipShownTimerFired();
}
}
void TooltipController::OnMouseEvent(ui::MouseEvent* event) {
switch (event->type()) {
case ui::ET_MOUSE_CAPTURE_CHANGED:
case ui::ET_MOUSE_EXITED:
case ui::ET_MOUSE_MOVED:
case ui::ET_MOUSE_DRAGGED: {
curr_mouse_loc_ = event->location();
aura::Window* target = NULL;
// Avoid a call to gfx::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, &curr_mouse_loc_);
}
SetTooltipWindow(target);
if (tooltip_timer_.IsRunning())
tooltip_timer_.Reset();
if (tooltip_->IsVisible())
UpdateIfRequired();
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_ = target;
if (target)
tooltip_text_at_mouse_press_ = aura::client::GetTooltipText(target);
}
tooltip_->Hide();
break;
case ui::ET_MOUSEWHEEL:
// Hide the tooltip for click, release, drag, wheel events.
if (tooltip_->IsVisible())
tooltip_->Hide();
break;
default:
break;
}
}
void TooltipController::OnTouchEvent(ui::TouchEvent* event) {
// TODO(varunjain): need to properly implement tooltips for
// touch events.
// Hide the tooltip for touch events.
tooltip_->Hide();
SetTooltipWindow(NULL);
}
void TooltipController::OnCancelMode(ui::CancelModeEvent* event) {
tooltip_->Hide();
SetTooltipWindow(NULL);
}
void TooltipController::OnWindowDestroyed(aura::Window* window) {
if (tooltip_window_ == window) {
tooltip_->Hide();
tooltip_shown_timeout_map_.erase(tooltip_window_);
tooltip_window_ = NULL;
}
}
////////////////////////////////////////////////////////////////////////////////
// TooltipController private:
void TooltipController::TooltipTimerFired() {
UpdateIfRequired();
}
void TooltipController::TooltipShownTimerFired() {
tooltip_->Hide();
// Since the user presumably no longer needs the tooltip, we also stop the
// tooltip timer so that tooltip does not pop back up. We will restart this
// timer if the tooltip changes (see UpdateTooltip()).
tooltip_timer_.Stop();
}
void TooltipController::UpdateIfRequired() {
if (!tooltips_enabled_ ||
aura::Env::GetInstance()->IsMouseButtonDown() ||
IsDragDropInProgress() || !IsCursorVisible()) {
tooltip_->Hide();
return;
}
base::string16 tooltip_text;
if (tooltip_window_)
tooltip_text = aura::client::GetTooltipText(tooltip_window_);
// If the user pressed a mouse button. We will hide the tooltip and not show
// it until there is a change in the tooltip.
if (tooltip_window_at_mouse_press_) {
if (tooltip_window_ == tooltip_window_at_mouse_press_ &&
tooltip_text == tooltip_text_at_mouse_press_) {
tooltip_->Hide();
return;
}
tooltip_window_at_mouse_press_ = NULL;
}
// If the uniqueness indicator is different from the previously encountered
// one, we should force tooltip update
const void* tooltip_id = aura::client::GetTooltipId(tooltip_window_);
bool ids_differ = false;
ids_differ = tooltip_id_ != tooltip_id;
tooltip_id_ = tooltip_id;
// We add the !tooltip_->IsVisible() below because when we come here from
// TooltipTimerFired(), the tooltip_text may not have changed but we still
// want to update the tooltip because the timer has fired.
// If we come here from UpdateTooltip(), we have already checked for tooltip
// visibility and this check below will have no effect.
if (tooltip_text_ != tooltip_text || !tooltip_->IsVisible() || ids_differ) {
tooltip_shown_timer_.Stop();
tooltip_text_ = tooltip_text;
base::string16 trimmed_text =
gfx::TruncateString(tooltip_text_, kMaxTooltipLength, gfx::WORD_BREAK);
// If the string consists entirely of whitespace, then don't both showing it
// (an empty tooltip is useless).
base::string16 whitespace_removed_text;
base::TrimWhitespace(trimmed_text, base::TRIM_ALL,
&whitespace_removed_text);
if (whitespace_removed_text.empty()) {
tooltip_->Hide();
} else {
gfx::Point widget_loc = curr_mouse_loc_ +
tooltip_window_->GetBoundsInScreen().OffsetFromOrigin();
tooltip_->SetText(tooltip_window_, whitespace_removed_text, widget_loc);
tooltip_->Show();
int timeout = GetTooltipShownTimeout();
if (timeout > 0) {
tooltip_shown_timer_.Start(FROM_HERE,
base::TimeDelta::FromMilliseconds(timeout),
this, &TooltipController::TooltipShownTimerFired);
}
}
}
}
bool TooltipController::IsTooltipVisible() {
return tooltip_->IsVisible();
}
bool TooltipController::IsDragDropInProgress() {
if (!tooltip_window_)
return false;
aura::client::DragDropClient* client =
aura::client::GetDragDropClient(tooltip_window_->GetRootWindow());
return client && client->IsDragDropInProgress();
}
bool TooltipController::IsCursorVisible() {
if (!tooltip_window_)
return false;
aura::Window* root = tooltip_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();
}
int TooltipController::GetTooltipShownTimeout() {
std::map<aura::Window*, int>::const_iterator it =
tooltip_shown_timeout_map_.find(tooltip_window_);
if (it == tooltip_shown_timeout_map_.end())
return kDefaultTooltipShownTimeoutMs;
return it->second;
}
void TooltipController::SetTooltipWindow(aura::Window* target) {
if (tooltip_window_ == target)
return;
if (tooltip_window_)
tooltip_window_->RemoveObserver(this);
tooltip_window_ = target;
if (tooltip_window_)
tooltip_window_->AddObserver(this);
}
} // namespace corewm
} // namespace views