| // 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 |