1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "ui/views/corewm/tooltip_controller.h"
6
7#include <vector>
8
9#include "base/strings/string_util.h"
10#include "base/time/time.h"
11#include "ui/aura/client/capture_client.h"
12#include "ui/aura/client/cursor_client.h"
13#include "ui/aura/client/screen_position_client.h"
14#include "ui/aura/env.h"
15#include "ui/aura/window.h"
16#include "ui/events/event.h"
17#include "ui/gfx/font.h"
18#include "ui/gfx/rect.h"
19#include "ui/gfx/screen.h"
20#include "ui/views/corewm/tooltip.h"
21#include "ui/views/widget/tooltip_manager.h"
22#include "ui/wm/public/drag_drop_client.h"
23
24namespace views {
25namespace corewm {
26namespace {
27
28const int kTooltipTimeoutMs = 500;
29const int kDefaultTooltipShownTimeoutMs = 10000;
30
31// Returns true if |target| is a valid window to get the tooltip from.
32// |event_target| is the original target from the event and |target| the window
33// at the same location.
34bool IsValidTarget(aura::Window* event_target, aura::Window* target) {
35  if (!target || (event_target == target))
36    return true;
37
38  void* event_target_grouping_id = event_target->GetNativeWindowProperty(
39      TooltipManager::kGroupingPropertyKey);
40  void* target_grouping_id = target->GetNativeWindowProperty(
41      TooltipManager::kGroupingPropertyKey);
42  return event_target_grouping_id &&
43      event_target_grouping_id == target_grouping_id;
44}
45
46// Returns the target (the Window tooltip text comes from) based on the event.
47// If a Window other than event.target() is returned, |location| is adjusted
48// to be in the coordinates of the returned Window.
49aura::Window* GetTooltipTarget(const ui::MouseEvent& event,
50                               gfx::Point* location) {
51  switch (event.type()) {
52    case ui::ET_MOUSE_CAPTURE_CHANGED:
53      // On windows we can get a capture changed without an exit. We need to
54      // reset state when this happens else the tooltip may incorrectly show.
55      return NULL;
56    case ui::ET_MOUSE_EXITED:
57      return NULL;
58    case ui::ET_MOUSE_MOVED:
59    case ui::ET_MOUSE_DRAGGED: {
60      aura::Window* event_target = static_cast<aura::Window*>(event.target());
61      if (!event_target)
62        return NULL;
63
64      // If a window other than |event_target| has capture, ignore the event.
65      // This can happen when RootWindow creates events when showing/hiding, or
66      // the system generates an extra event. We have to check
67      // GetGlobalCaptureWindow() as Windows does not use a singleton
68      // CaptureClient.
69      if (!event_target->HasCapture()) {
70        aura::Window* root = event_target->GetRootWindow();
71        if (root) {
72          aura::client::CaptureClient* capture_client =
73              aura::client::GetCaptureClient(root);
74          if (capture_client) {
75            aura::Window* capture_window =
76                capture_client->GetGlobalCaptureWindow();
77            if (capture_window && event_target != capture_window)
78              return NULL;
79          }
80        }
81        return event_target;
82      }
83
84      // If |target| has capture all events go to it, even if the mouse is
85      // really over another window. Find the real window the mouse is over.
86      gfx::Point screen_loc(event.location());
87      aura::client::GetScreenPositionClient(event_target->GetRootWindow())->
88          ConvertPointToScreen(event_target, &screen_loc);
89      gfx::Screen* screen = gfx::Screen::GetScreenFor(event_target);
90      aura::Window* target = screen->GetWindowAtScreenPoint(screen_loc);
91      if (!target)
92        return NULL;
93      gfx::Point target_loc(screen_loc);
94      aura::client::GetScreenPositionClient(target->GetRootWindow())->
95          ConvertPointFromScreen(target, &target_loc);
96      aura::Window* screen_target = target->GetEventHandlerForPoint(target_loc);
97      if (!IsValidTarget(event_target, screen_target))
98        return NULL;
99
100      aura::Window::ConvertPointToTarget(screen_target, target, &target_loc);
101      *location = target_loc;
102      return screen_target;
103    }
104    default:
105      NOTREACHED();
106      break;
107  }
108  return NULL;
109}
110
111}  // namespace
112
113////////////////////////////////////////////////////////////////////////////////
114// TooltipController public:
115
116TooltipController::TooltipController(scoped_ptr<Tooltip> tooltip)
117    : tooltip_window_(NULL),
118      tooltip_id_(NULL),
119      tooltip_window_at_mouse_press_(NULL),
120      tooltip_(tooltip.Pass()),
121      tooltips_enabled_(true) {
122  tooltip_timer_.Start(FROM_HERE,
123      base::TimeDelta::FromMilliseconds(kTooltipTimeoutMs),
124      this, &TooltipController::TooltipTimerFired);
125}
126
127TooltipController::~TooltipController() {
128  if (tooltip_window_)
129    tooltip_window_->RemoveObserver(this);
130}
131
132void TooltipController::UpdateTooltip(aura::Window* target) {
133  // If tooltip is visible, we may want to hide it. If it is not, we are ok.
134  if (tooltip_window_ == target && tooltip_->IsVisible())
135    UpdateIfRequired();
136
137  // Reset |tooltip_window_at_mouse_press_| if the moving within the same window
138  // but over a region that has different tooltip text. By resetting
139  // |tooltip_window_at_mouse_press_| we ensure the next time the timer fires
140  // we'll requery for the tooltip text.
141  // This handles the case of clicking on a view, moving within the same window
142  // but over a different view, than back to the original.
143  if (tooltip_window_at_mouse_press_ &&
144      target == tooltip_window_at_mouse_press_ &&
145      aura::client::GetTooltipText(target) != tooltip_text_at_mouse_press_) {
146    tooltip_window_at_mouse_press_ = NULL;
147  }
148
149  // If we had stopped the tooltip timer for some reason, we must restart it if
150  // there is a change in the tooltip.
151  if (!tooltip_timer_.IsRunning()) {
152    if (tooltip_window_ != target || (tooltip_window_ &&
153        tooltip_text_ != aura::client::GetTooltipText(tooltip_window_))) {
154      tooltip_timer_.Start(FROM_HERE,
155          base::TimeDelta::FromMilliseconds(kTooltipTimeoutMs),
156          this, &TooltipController::TooltipTimerFired);
157    }
158  }
159}
160
161void TooltipController::SetTooltipShownTimeout(aura::Window* target,
162                                               int timeout_in_ms) {
163  tooltip_shown_timeout_map_[target] = timeout_in_ms;
164}
165
166void TooltipController::SetTooltipsEnabled(bool enable) {
167  if (tooltips_enabled_ == enable)
168    return;
169  tooltips_enabled_ = enable;
170  UpdateTooltip(tooltip_window_);
171}
172
173void TooltipController::OnKeyEvent(ui::KeyEvent* event) {
174  // On key press, we want to hide the tooltip and not show it until change.
175  // This is the same behavior as hiding tooltips on timeout. Hence, we can
176  // simply simulate a timeout.
177  if (tooltip_shown_timer_.IsRunning()) {
178    tooltip_shown_timer_.Stop();
179    TooltipShownTimerFired();
180  }
181}
182
183void TooltipController::OnMouseEvent(ui::MouseEvent* event) {
184  switch (event->type()) {
185    case ui::ET_MOUSE_CAPTURE_CHANGED:
186    case ui::ET_MOUSE_EXITED:
187    case ui::ET_MOUSE_MOVED:
188    case ui::ET_MOUSE_DRAGGED: {
189      curr_mouse_loc_ = event->location();
190      aura::Window* target = NULL;
191      // Avoid a call to gfx::Screen::GetWindowAtScreenPoint() since it can be
192      // very expensive on X11 in cases when the tooltip is hidden anyway.
193      if (tooltips_enabled_ &&
194          !aura::Env::GetInstance()->IsMouseButtonDown() &&
195          !IsDragDropInProgress()) {
196        target = GetTooltipTarget(*event, &curr_mouse_loc_);
197      }
198      SetTooltipWindow(target);
199      if (tooltip_timer_.IsRunning())
200        tooltip_timer_.Reset();
201
202      if (tooltip_->IsVisible())
203        UpdateIfRequired();
204      break;
205    }
206    case ui::ET_MOUSE_PRESSED:
207      if ((event->flags() & ui::EF_IS_NON_CLIENT) == 0) {
208        aura::Window* target = static_cast<aura::Window*>(event->target());
209        // We don't get a release for non-client areas.
210        tooltip_window_at_mouse_press_ = target;
211        if (target)
212          tooltip_text_at_mouse_press_ = aura::client::GetTooltipText(target);
213      }
214      tooltip_->Hide();
215      break;
216    case ui::ET_MOUSEWHEEL:
217      // Hide the tooltip for click, release, drag, wheel events.
218      if (tooltip_->IsVisible())
219        tooltip_->Hide();
220      break;
221    default:
222      break;
223  }
224}
225
226void TooltipController::OnTouchEvent(ui::TouchEvent* event) {
227  // TODO(varunjain): need to properly implement tooltips for
228  // touch events.
229  // Hide the tooltip for touch events.
230  tooltip_->Hide();
231  SetTooltipWindow(NULL);
232}
233
234void TooltipController::OnCancelMode(ui::CancelModeEvent* event) {
235  tooltip_->Hide();
236  SetTooltipWindow(NULL);
237}
238
239void TooltipController::OnWindowDestroyed(aura::Window* window) {
240  if (tooltip_window_ == window) {
241    tooltip_->Hide();
242    tooltip_shown_timeout_map_.erase(tooltip_window_);
243    tooltip_window_ = NULL;
244  }
245}
246
247////////////////////////////////////////////////////////////////////////////////
248// TooltipController private:
249
250void TooltipController::TooltipTimerFired() {
251  UpdateIfRequired();
252}
253
254void TooltipController::TooltipShownTimerFired() {
255  tooltip_->Hide();
256
257  // Since the user presumably no longer needs the tooltip, we also stop the
258  // tooltip timer so that tooltip does not pop back up. We will restart this
259  // timer if the tooltip changes (see UpdateTooltip()).
260  tooltip_timer_.Stop();
261}
262
263void TooltipController::UpdateIfRequired() {
264  if (!tooltips_enabled_ ||
265      aura::Env::GetInstance()->IsMouseButtonDown() ||
266      IsDragDropInProgress() || !IsCursorVisible()) {
267    tooltip_->Hide();
268    return;
269  }
270
271  base::string16 tooltip_text;
272  if (tooltip_window_)
273    tooltip_text = aura::client::GetTooltipText(tooltip_window_);
274
275  // If the user pressed a mouse button. We will hide the tooltip and not show
276  // it until there is a change in the tooltip.
277  if (tooltip_window_at_mouse_press_) {
278    if (tooltip_window_ == tooltip_window_at_mouse_press_ &&
279        tooltip_text == tooltip_text_at_mouse_press_) {
280      tooltip_->Hide();
281      return;
282    }
283    tooltip_window_at_mouse_press_ = NULL;
284  }
285
286  // If the uniqueness indicator is different from the previously encountered
287  // one, we should force tooltip update
288  const void* tooltip_id = aura::client::GetTooltipId(tooltip_window_);
289  bool ids_differ = false;
290  ids_differ = tooltip_id_ != tooltip_id;
291  tooltip_id_ = tooltip_id;
292
293  // We add the !tooltip_->IsVisible() below because when we come here from
294  // TooltipTimerFired(), the tooltip_text may not have changed but we still
295  // want to update the tooltip because the timer has fired.
296  // If we come here from UpdateTooltip(), we have already checked for tooltip
297  // visibility and this check below will have no effect.
298  if (tooltip_text_ != tooltip_text || !tooltip_->IsVisible() || ids_differ) {
299    tooltip_shown_timer_.Stop();
300    tooltip_text_ = tooltip_text;
301    base::string16 trimmed_text(tooltip_text_);
302    views::TooltipManager::TrimTooltipText(&trimmed_text);
303    // If the string consists entirely of whitespace, then don't both showing it
304    // (an empty tooltip is useless).
305    base::string16 whitespace_removed_text;
306    base::TrimWhitespace(trimmed_text, base::TRIM_ALL,
307                         &whitespace_removed_text);
308    if (whitespace_removed_text.empty()) {
309      tooltip_->Hide();
310    } else {
311      gfx::Point widget_loc = curr_mouse_loc_ +
312          tooltip_window_->GetBoundsInScreen().OffsetFromOrigin();
313      tooltip_->SetText(tooltip_window_, whitespace_removed_text, widget_loc);
314      tooltip_->Show();
315      int timeout = GetTooltipShownTimeout();
316      if (timeout > 0) {
317        tooltip_shown_timer_.Start(FROM_HERE,
318            base::TimeDelta::FromMilliseconds(timeout),
319            this, &TooltipController::TooltipShownTimerFired);
320      }
321    }
322  }
323}
324
325bool TooltipController::IsTooltipVisible() {
326  return tooltip_->IsVisible();
327}
328
329bool TooltipController::IsDragDropInProgress() {
330  if (!tooltip_window_)
331    return false;
332  aura::client::DragDropClient* client =
333      aura::client::GetDragDropClient(tooltip_window_->GetRootWindow());
334  return client && client->IsDragDropInProgress();
335}
336
337bool TooltipController::IsCursorVisible() {
338  if (!tooltip_window_)
339    return false;
340  aura::Window* root = tooltip_window_->GetRootWindow();
341  if (!root)
342    return false;
343  aura::client::CursorClient* cursor_client =
344      aura::client::GetCursorClient(root);
345  // |cursor_client| may be NULL in tests, treat NULL as always visible.
346  return !cursor_client || cursor_client->IsCursorVisible();
347}
348
349int TooltipController::GetTooltipShownTimeout() {
350  std::map<aura::Window*, int>::const_iterator it =
351      tooltip_shown_timeout_map_.find(tooltip_window_);
352  if (it == tooltip_shown_timeout_map_.end())
353    return kDefaultTooltipShownTimeoutMs;
354  return it->second;
355}
356
357void TooltipController::SetTooltipWindow(aura::Window* target) {
358  if (tooltip_window_ == target)
359    return;
360  if (tooltip_window_)
361    tooltip_window_->RemoveObserver(this);
362  tooltip_window_ = target;
363  if (tooltip_window_)
364    tooltip_window_->AddObserver(this);
365}
366
367}  // namespace corewm
368}  // namespace views
369