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/command_line.h" 10 #include "base/location.h" 11 #include "base/strings/string_split.h" 12 #include "base/time/time.h" 13 #include "ui/aura/client/cursor_client.h" 14 #include "ui/aura/client/drag_drop_client.h" 15 #include "ui/aura/env.h" 16 #include "ui/aura/root_window.h" 17 #include "ui/aura/window.h" 18 #include "ui/base/events/event.h" 19 #include "ui/base/resource/resource_bundle.h" 20 #include "ui/base/text/text_elider.h" 21 #include "ui/gfx/font.h" 22 #include "ui/gfx/point.h" 23 #include "ui/gfx/rect.h" 24 #include "ui/gfx/screen.h" 25 #include "ui/views/background.h" 26 #include "ui/views/border.h" 27 #include "ui/views/controls/label.h" 28 #include "ui/views/corewm/corewm_switches.h" 29 #include "ui/views/widget/widget.h" 30 #include "ui/views/widget/widget_observer.h" 31 32 namespace { 33 34 const SkColor kTooltipBackground = 0xFFFFFFCC; 35 const SkColor kTooltipBorder = 0xFF646450; 36 const int kTooltipBorderWidth = 1; 37 const int kTooltipHorizontalPadding = 3; 38 39 // Max visual tooltip width. If a tooltip is greater than this width, it will 40 // be wrapped. 41 const int kTooltipMaxWidthPixels = 400; 42 43 // Maximum number of lines we allow in the tooltip. 44 const size_t kMaxLines = 10; 45 46 // TODO(derat): This padding is needed on Chrome OS devices but seems excessive 47 // when running the same binary on a Linux workstation; presumably there's a 48 // difference in font metrics. Rationalize this. 49 const int kTooltipVerticalPadding = 2; 50 const int kTooltipTimeoutMs = 500; 51 const int kDefaultTooltipShownTimeoutMs = 10000; 52 53 // FIXME: get cursor offset from actual cursor size. 54 const int kCursorOffsetX = 10; 55 const int kCursorOffsetY = 15; 56 57 // Maximum number of characters we allow in a tooltip. 58 const size_t kMaxTooltipLength = 1024; 59 60 gfx::Font GetDefaultFont() { 61 // TODO(varunjain): implementation duplicated in tooltip_manager_aura. Figure 62 // out a way to merge. 63 return ui::ResourceBundle::GetSharedInstance().GetFont( 64 ui::ResourceBundle::BaseFont); 65 } 66 67 // Creates a widget of type TYPE_TOOLTIP 68 views::Widget* CreateTooltip(aura::Window* tooltip_window) { 69 views::Widget* widget = new views::Widget; 70 views::Widget::InitParams params; 71 // For aura, since we set the type to TOOLTIP_TYPE, the widget will get 72 // auto-parented to the MenuAndTooltipsContainer. 73 params.type = views::Widget::InitParams::TYPE_TOOLTIP; 74 params.context = tooltip_window; 75 DCHECK(params.context); 76 params.keep_on_top = true; 77 params.accept_events = false; 78 widget->Init(params); 79 return widget; 80 } 81 82 } // namespace 83 84 namespace views { 85 namespace corewm { 86 87 // Displays a widget with tooltip using a views::Label. 88 class TooltipController::Tooltip : public views::WidgetObserver { 89 public: 90 Tooltip(TooltipController* controller) 91 : controller_(controller), 92 widget_(NULL) { 93 label_.set_background( 94 views::Background::CreateSolidBackground(kTooltipBackground)); 95 if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kNoDropShadows)) { 96 label_.set_border( 97 views::Border::CreateSolidBorder(kTooltipBorderWidth, 98 kTooltipBorder)); 99 } 100 label_.set_owned_by_client(); 101 label_.SetMultiLine(true); 102 } 103 104 virtual ~Tooltip() { 105 if (widget_) { 106 widget_->RemoveObserver(this); 107 widget_->Close(); 108 } 109 } 110 111 // Updates the text on the tooltip and resizes to fit. 112 void SetText(aura::Window* window, 113 const string16& tooltip_text, 114 const gfx::Point& location) { 115 int max_width, line_count; 116 string16 trimmed_text(tooltip_text); 117 controller_->TrimTooltipToFit( 118 controller_->GetMaxWidth(location), &trimmed_text, &max_width, 119 &line_count); 120 label_.SetText(trimmed_text); 121 122 int width = max_width + 2 * kTooltipHorizontalPadding; 123 int height = label_.GetHeightForWidth(max_width) + 124 2 * kTooltipVerticalPadding; 125 if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kNoDropShadows)) { 126 width += 2 * kTooltipBorderWidth; 127 height += 2 * kTooltipBorderWidth; 128 } 129 CreateWidgetIfNecessary(window); 130 SetTooltipBounds(location, width, height); 131 } 132 133 // Shows the tooltip. 134 void Show() { 135 if (widget_) 136 widget_->Show(); 137 } 138 139 // Hides the tooltip. 140 void Hide() { 141 if (widget_) 142 widget_->Hide(); 143 } 144 145 bool IsVisible() { 146 return widget_ && widget_->IsVisible(); 147 } 148 149 // Overriden from views::WidgetObserver. 150 virtual void OnWidgetDestroying(views::Widget* widget) OVERRIDE { 151 DCHECK_EQ(widget_, widget); 152 widget_ = NULL; 153 } 154 155 private: 156 // Adjusts the bounds given by the arguments to fit inside the desktop 157 // and applies the adjusted bounds to the label_. 158 void SetTooltipBounds(const gfx::Point& mouse_pos, 159 int tooltip_width, 160 int tooltip_height) { 161 gfx::Rect tooltip_rect(mouse_pos.x(), mouse_pos.y(), tooltip_width, 162 tooltip_height); 163 164 tooltip_rect.Offset(kCursorOffsetX, kCursorOffsetY); 165 gfx::Rect display_bounds = controller_->GetBoundsForTooltip(mouse_pos); 166 167 // If tooltip is out of bounds on the x axis, we simply shift it 168 // horizontally by the offset. 169 if (tooltip_rect.right() > display_bounds.right()) { 170 int h_offset = tooltip_rect.right() - display_bounds.right(); 171 tooltip_rect.Offset(-h_offset, 0); 172 } 173 174 // If tooltip is out of bounds on the y axis, we flip it to appear above the 175 // mouse cursor instead of below. 176 if (tooltip_rect.bottom() > display_bounds.bottom()) 177 tooltip_rect.set_y(mouse_pos.y() - tooltip_height); 178 179 tooltip_rect.AdjustToFit(display_bounds); 180 widget_->SetBounds(tooltip_rect); 181 } 182 183 void CreateWidgetIfNecessary(aura::Window* tooltip_window) { 184 if (widget_) 185 return; 186 widget_ = CreateTooltip(tooltip_window); 187 widget_->SetContentsView(&label_); 188 widget_->AddObserver(this); 189 } 190 191 views::Label label_; 192 TooltipController* controller_; 193 views::Widget* widget_; 194 195 DISALLOW_COPY_AND_ASSIGN(Tooltip); 196 }; 197 198 //////////////////////////////////////////////////////////////////////////////// 199 // TooltipController public: 200 201 TooltipController::TooltipController(gfx::ScreenType screen_type) 202 : screen_type_(screen_type), 203 tooltip_window_(NULL), 204 tooltip_window_at_mouse_press_(NULL), 205 mouse_pressed_(false), 206 tooltips_enabled_(true) { 207 tooltip_timer_.Start(FROM_HERE, 208 base::TimeDelta::FromMilliseconds(kTooltipTimeoutMs), 209 this, &TooltipController::TooltipTimerFired); 210 } 211 212 TooltipController::~TooltipController() { 213 if (tooltip_window_) 214 tooltip_window_->RemoveObserver(this); 215 } 216 217 void TooltipController::UpdateTooltip(aura::Window* target) { 218 // If tooltip is visible, we may want to hide it. If it is not, we are ok. 219 if (tooltip_window_ == target && GetTooltip()->IsVisible()) 220 UpdateIfRequired(); 221 222 // If we had stopped the tooltip timer for some reason, we must restart it if 223 // there is a change in the tooltip. 224 if (!tooltip_timer_.IsRunning()) { 225 if (tooltip_window_ != target || (tooltip_window_ && 226 tooltip_text_ != aura::client::GetTooltipText(tooltip_window_))) { 227 tooltip_timer_.Start(FROM_HERE, 228 base::TimeDelta::FromMilliseconds(kTooltipTimeoutMs), 229 this, &TooltipController::TooltipTimerFired); 230 } 231 } 232 } 233 234 void TooltipController::SetTooltipShownTimeout(aura::Window* target, 235 int timeout_in_ms) { 236 tooltip_shown_timeout_map_[target] = timeout_in_ms; 237 } 238 239 void TooltipController::SetTooltipsEnabled(bool enable) { 240 if (tooltips_enabled_ == enable) 241 return; 242 tooltips_enabled_ = enable; 243 UpdateTooltip(tooltip_window_); 244 } 245 246 void TooltipController::OnKeyEvent(ui::KeyEvent* event) { 247 // On key press, we want to hide the tooltip and not show it until change. 248 // This is the same behavior as hiding tooltips on timeout. Hence, we can 249 // simply simulate a timeout. 250 if (tooltip_shown_timer_.IsRunning()) { 251 tooltip_shown_timer_.Stop(); 252 TooltipShownTimerFired(); 253 } 254 } 255 256 void TooltipController::OnMouseEvent(ui::MouseEvent* event) { 257 aura::Window* target = static_cast<aura::Window*>(event->target()); 258 switch (event->type()) { 259 case ui::ET_MOUSE_MOVED: 260 case ui::ET_MOUSE_DRAGGED: 261 if (tooltip_window_ != target) { 262 if (tooltip_window_) 263 tooltip_window_->RemoveObserver(this); 264 tooltip_window_ = target; 265 tooltip_window_->AddObserver(this); 266 } 267 curr_mouse_loc_ = event->location(); 268 if (tooltip_timer_.IsRunning()) 269 tooltip_timer_.Reset(); 270 271 if (GetTooltip()->IsVisible()) 272 UpdateIfRequired(); 273 break; 274 case ui::ET_MOUSE_PRESSED: 275 if ((event->flags() & ui::EF_IS_NON_CLIENT) == 0) { 276 // We don't get a release for non-client areas. 277 mouse_pressed_ = true; 278 tooltip_window_at_mouse_press_ = target; 279 if (target) 280 tooltip_text_at_mouse_press_ = aura::client::GetTooltipText(target); 281 } 282 GetTooltip()->Hide(); 283 break; 284 case ui::ET_MOUSE_RELEASED: 285 mouse_pressed_ = false; 286 break; 287 case ui::ET_MOUSE_CAPTURE_CHANGED: 288 // We will not received a mouse release, so reset mouse pressed state. 289 mouse_pressed_ = false; 290 case ui::ET_MOUSEWHEEL: 291 // Hide the tooltip for click, release, drag, wheel events. 292 if (GetTooltip()->IsVisible()) 293 GetTooltip()->Hide(); 294 break; 295 default: 296 break; 297 } 298 } 299 300 void TooltipController::OnTouchEvent(ui::TouchEvent* event) { 301 // TODO(varunjain): need to properly implement tooltips for 302 // touch events. 303 // Hide the tooltip for touch events. 304 if (GetTooltip()->IsVisible()) 305 GetTooltip()->Hide(); 306 if (tooltip_window_) 307 tooltip_window_->RemoveObserver(this); 308 tooltip_window_ = NULL; 309 } 310 311 void TooltipController::OnCancelMode(ui::CancelModeEvent* event) { 312 if (tooltip_.get() && tooltip_->IsVisible()) 313 tooltip_->Hide(); 314 } 315 316 void TooltipController::OnWindowDestroyed(aura::Window* window) { 317 if (tooltip_window_ == window) { 318 tooltip_shown_timeout_map_.erase(tooltip_window_); 319 tooltip_window_->RemoveObserver(this); 320 tooltip_window_ = NULL; 321 } 322 } 323 324 //////////////////////////////////////////////////////////////////////////////// 325 // TooltipController private: 326 327 int TooltipController::GetMaxWidth(const gfx::Point& location) const { 328 // TODO(varunjain): implementation duplicated in tooltip_manager_aura. Figure 329 // out a way to merge. 330 gfx::Rect display_bounds = GetBoundsForTooltip(location); 331 return (display_bounds.width() + 1) / 2; 332 } 333 334 gfx::Rect TooltipController::GetBoundsForTooltip( 335 const gfx::Point& origin) const { 336 DCHECK(tooltip_window_); 337 gfx::Rect widget_bounds; 338 // For Desktop aura we constrain the tooltip to the bounds of the Widget 339 // (which comes from the RootWindow). 340 if (screen_type_ == gfx::SCREEN_TYPE_NATIVE && 341 gfx::SCREEN_TYPE_NATIVE != gfx::SCREEN_TYPE_ALTERNATE) { 342 aura::RootWindow* root = tooltip_window_->GetRootWindow(); 343 widget_bounds = gfx::Rect(root->GetHostOrigin(), root->GetHostSize()); 344 } 345 gfx::Screen* screen = gfx::Screen::GetScreenByType(screen_type_); 346 gfx::Rect bounds(screen->GetDisplayNearestPoint(origin).bounds()); 347 if (!widget_bounds.IsEmpty()) 348 bounds.Intersect(widget_bounds); 349 return bounds; 350 } 351 352 // static 353 void TooltipController::TrimTooltipToFit(int max_width, 354 string16* text, 355 int* width, 356 int* line_count) { 357 *width = 0; 358 *line_count = 0; 359 360 // Clamp the tooltip length to kMaxTooltipLength so that we don't 361 // accidentally DOS the user with a mega tooltip. 362 if (text->length() > kMaxTooltipLength) 363 *text = text->substr(0, kMaxTooltipLength); 364 365 // Determine the available width for the tooltip. 366 int available_width = std::min(kTooltipMaxWidthPixels, max_width); 367 368 std::vector<string16> lines; 369 base::SplitString(*text, '\n', &lines); 370 std::vector<string16> result_lines; 371 372 // Format each line to fit. 373 gfx::Font font = GetDefaultFont(); 374 for (std::vector<string16>::iterator l = lines.begin(); l != lines.end(); 375 ++l) { 376 // We break the line at word boundaries, then stuff as many words as we can 377 // in the available width to the current line, and move the remaining words 378 // to a new line. 379 std::vector<string16> words; 380 base::SplitStringDontTrim(*l, ' ', &words); 381 int current_width = 0; 382 string16 line; 383 for (std::vector<string16>::iterator w = words.begin(); w != words.end(); 384 ++w) { 385 string16 word = *w; 386 if (w + 1 != words.end()) 387 word.push_back(' '); 388 int word_width = font.GetStringWidth(word); 389 if (current_width + word_width > available_width) { 390 // Current width will exceed the available width. Must start a new line. 391 if (!line.empty()) 392 result_lines.push_back(line); 393 current_width = 0; 394 line.clear(); 395 } 396 current_width += word_width; 397 line.append(word); 398 } 399 result_lines.push_back(line); 400 } 401 402 // Clamp number of lines to |kMaxLines|. 403 if (result_lines.size() > kMaxLines) { 404 result_lines.resize(kMaxLines); 405 // Add ellipses character to last line. 406 result_lines[kMaxLines - 1] = ui::TruncateString( 407 result_lines.back(), result_lines.back().length() - 1); 408 } 409 *line_count = result_lines.size(); 410 411 // Flatten the result. 412 string16 result; 413 for (std::vector<string16>::iterator l = result_lines.begin(); 414 l != result_lines.end(); ++l) { 415 if (!result.empty()) 416 result.push_back('\n'); 417 int line_width = font.GetStringWidth(*l); 418 // Since we only break at word boundaries, it could happen that due to some 419 // very long word, line_width is greater than the available_width. In such 420 // case, we simply truncate at available_width and add ellipses at the end. 421 if (line_width > available_width) { 422 *width = available_width; 423 result.append(ui::ElideText(*l, font, available_width, ui::ELIDE_AT_END)); 424 } else { 425 *width = std::max(*width, line_width); 426 result.append(*l); 427 } 428 } 429 *text = result; 430 } 431 432 void TooltipController::TooltipTimerFired() { 433 UpdateIfRequired(); 434 } 435 436 void TooltipController::TooltipShownTimerFired() { 437 GetTooltip()->Hide(); 438 439 // Since the user presumably no longer needs the tooltip, we also stop the 440 // tooltip timer so that tooltip does not pop back up. We will restart this 441 // timer if the tooltip changes (see UpdateTooltip()). 442 tooltip_timer_.Stop(); 443 } 444 445 void TooltipController::UpdateIfRequired() { 446 if (!tooltips_enabled_ || mouse_pressed_ || IsDragDropInProgress() || 447 !IsCursorVisible()) { 448 GetTooltip()->Hide(); 449 return; 450 } 451 452 string16 tooltip_text; 453 if (tooltip_window_) 454 tooltip_text = aura::client::GetTooltipText(tooltip_window_); 455 456 // If the user pressed a mouse button. We will hide the tooltip and not show 457 // it until there is a change in the tooltip. 458 if (tooltip_window_at_mouse_press_) { 459 if (tooltip_window_ == tooltip_window_at_mouse_press_ && 460 tooltip_text == tooltip_text_at_mouse_press_) { 461 GetTooltip()->Hide(); 462 return; 463 } 464 tooltip_window_at_mouse_press_ = NULL; 465 } 466 467 // We add the !GetTooltip()->IsVisible() below because when we come here from 468 // TooltipTimerFired(), the tooltip_text may not have changed but we still 469 // want to update the tooltip because the timer has fired. 470 // If we come here from UpdateTooltip(), we have already checked for tooltip 471 // visibility and this check below will have no effect. 472 if (tooltip_text_ != tooltip_text || !GetTooltip()->IsVisible()) { 473 tooltip_shown_timer_.Stop(); 474 tooltip_text_ = tooltip_text; 475 if (tooltip_text_.empty()) { 476 GetTooltip()->Hide(); 477 } else { 478 gfx::Point widget_loc = curr_mouse_loc_ + 479 tooltip_window_->GetBoundsInScreen().OffsetFromOrigin(); 480 gfx::Rect bounds(GetBoundsForTooltip(widget_loc)); 481 if (bounds.IsEmpty()) { 482 tooltip_text_.clear(); 483 GetTooltip()->Hide(); 484 } else { 485 GetTooltip()->SetText(tooltip_window_, tooltip_text_, widget_loc); 486 GetTooltip()->Show(); 487 int timeout = GetTooltipShownTimeout(); 488 if (timeout > 0) { 489 tooltip_shown_timer_.Start(FROM_HERE, 490 base::TimeDelta::FromMilliseconds(timeout), 491 this, &TooltipController::TooltipShownTimerFired); 492 } 493 } 494 } 495 } 496 } 497 498 bool TooltipController::IsTooltipVisible() { 499 return GetTooltip()->IsVisible(); 500 } 501 502 bool TooltipController::IsDragDropInProgress() { 503 if (!tooltip_window_) 504 return false; 505 aura::client::DragDropClient* client = 506 aura::client::GetDragDropClient(tooltip_window_->GetRootWindow()); 507 return client && client->IsDragDropInProgress(); 508 } 509 510 TooltipController::Tooltip* TooltipController::GetTooltip() { 511 if (!tooltip_.get()) 512 tooltip_.reset(new Tooltip(this)); 513 return tooltip_.get(); 514 } 515 516 bool TooltipController::IsCursorVisible() { 517 if (!tooltip_window_) 518 return false; 519 aura::RootWindow* root = tooltip_window_->GetRootWindow(); 520 if (!root) 521 return false; 522 aura::client::CursorClient* cursor_client = 523 aura::client::GetCursorClient(root); 524 // |cursor_client| may be NULL in tests, treat NULL as always visible. 525 return !cursor_client || cursor_client->IsCursorVisible(); 526 } 527 528 int TooltipController::GetTooltipShownTimeout() { 529 std::map<aura::Window*, int>::const_iterator it = 530 tooltip_shown_timeout_map_.find(tooltip_window_); 531 if (it == tooltip_shown_timeout_map_.end()) 532 return kDefaultTooltipShownTimeoutMs; 533 return it->second; 534 } 535 536 } // namespace corewm 537 } // namespace views 538