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/widget/tooltip_manager_win.h" 6 7 #include <windowsx.h> 8 9 #include <limits> 10 11 #include "base/bind.h" 12 #include "base/i18n/rtl.h" 13 #include "base/logging.h" 14 #include "base/message_loop/message_loop.h" 15 #include "base/strings/string_util.h" 16 #include "base/win/scoped_hdc.h" 17 #include "base/win/scoped_select_object.h" 18 #include "ui/base/l10n/l10n_util_win.h" 19 #include "ui/base/win/dpi.h" 20 #include "ui/base/win/hwnd_util.h" 21 #include "ui/base/win/scoped_set_map_mode.h" 22 #include "ui/gfx/font.h" 23 #include "ui/gfx/screen.h" 24 #include "ui/views/view.h" 25 #include "ui/views/widget/monitor_win.h" 26 #include "ui/views/widget/widget.h" 27 28 namespace views { 29 30 static int tooltip_height_ = 0; 31 32 // Default timeout for the tooltip displayed using keyboard. 33 // Timeout is measured in milliseconds. 34 static const int kDefaultTimeout = 4000; 35 36 // static 37 int TooltipManager::GetTooltipHeight() { 38 DCHECK_GT(tooltip_height_, 0); 39 return tooltip_height_; 40 } 41 42 static gfx::Font DetermineDefaultFont() { 43 HWND window = CreateWindowEx( 44 WS_EX_TRANSPARENT | l10n_util::GetExtendedTooltipStyles(), 45 TOOLTIPS_CLASS, NULL, 0 , 0, 0, 0, 0, NULL, NULL, NULL, NULL); 46 if (!window) 47 return gfx::Font(); 48 HFONT hfont = reinterpret_cast<HFONT>(SendMessage(window, WM_GETFONT, 0, 0)); 49 gfx::Font font = hfont ? gfx::Font(hfont) : gfx::Font(); 50 DestroyWindow(window); 51 return font; 52 } 53 54 // static 55 gfx::Font TooltipManager::GetDefaultFont() { 56 static gfx::Font* font = NULL; 57 if (!font) 58 font = new gfx::Font(DetermineDefaultFont()); 59 return *font; 60 } 61 62 // static 63 int TooltipManager::GetMaxWidth(int x, int y, gfx::NativeView context) { 64 gfx::Rect monitor_bounds = 65 gfx::Screen::GetScreenFor(context)->GetDisplayNearestPoint( 66 gfx::Point(x, y)).bounds(); 67 // Allow the tooltip to be almost as wide as the screen. 68 // Otherwise, we would truncate important text, since we're not word-wrapping 69 // the text onto multiple lines. 70 return monitor_bounds.width() == 0 ? 800 : monitor_bounds.width() - 30; 71 } 72 73 TooltipManagerWin::TooltipManagerWin(Widget* widget) 74 : widget_(widget), 75 tooltip_hwnd_(NULL), 76 last_mouse_pos_(-1, -1), 77 tooltip_showing_(false), 78 last_tooltip_view_(NULL), 79 last_view_out_of_sync_(false), 80 tooltip_width_(0), 81 keyboard_tooltip_hwnd_(NULL), 82 keyboard_tooltip_factory_(this) { 83 DCHECK(widget); 84 DCHECK(widget->GetNativeView()); 85 } 86 87 TooltipManagerWin::~TooltipManagerWin() { 88 if (tooltip_hwnd_) 89 DestroyWindow(tooltip_hwnd_); 90 if (keyboard_tooltip_hwnd_) 91 DestroyWindow(keyboard_tooltip_hwnd_); 92 } 93 94 bool TooltipManagerWin::Init() { 95 DCHECK(!tooltip_hwnd_); 96 // Create the tooltip control. 97 tooltip_hwnd_ = CreateWindowEx( 98 WS_EX_TRANSPARENT | l10n_util::GetExtendedTooltipStyles(), 99 TOOLTIPS_CLASS, NULL, TTS_NOPREFIX, 0, 0, 0, 0, 100 GetParent(), NULL, NULL, NULL); 101 if (!tooltip_hwnd_) 102 return false; 103 104 l10n_util::AdjustUIFontForWindow(tooltip_hwnd_); 105 106 // This effectively turns off clipping of tooltips. We need this otherwise 107 // multi-line text (\r\n) won't work right. The size doesn't really matter 108 // (just as long as its bigger than the monitor's width) as we clip to the 109 // screen size before rendering. 110 SendMessage(tooltip_hwnd_, TTM_SETMAXTIPWIDTH, 0, 111 std::numeric_limits<int16>::max()); 112 113 // Add one tool that is used for all tooltips. 114 toolinfo_.cbSize = sizeof(toolinfo_); 115 toolinfo_.uFlags = TTF_TRANSPARENT | TTF_IDISHWND; 116 toolinfo_.hwnd = GetParent(); 117 toolinfo_.uId = reinterpret_cast<UINT_PTR>(GetParent()); 118 // Setting this tells windows to call GetParent() back (using a WM_NOTIFY 119 // message) for the actual tooltip contents. 120 toolinfo_.lpszText = LPSTR_TEXTCALLBACK; 121 toolinfo_.lpReserved = NULL; 122 SetRectEmpty(&toolinfo_.rect); 123 SendMessage(tooltip_hwnd_, TTM_ADDTOOL, 0, (LPARAM)&toolinfo_); 124 return true; 125 } 126 127 gfx::NativeView TooltipManagerWin::GetParent() { 128 return widget_->GetNativeView(); 129 } 130 131 void TooltipManagerWin::UpdateTooltip() { 132 // Set last_view_out_of_sync_ to indicate the view is currently out of sync. 133 // This doesn't update the view under the mouse immediately as it may cause 134 // timing problems. 135 last_view_out_of_sync_ = true; 136 last_tooltip_view_ = NULL; 137 // Hide the tooltip. 138 SendMessage(tooltip_hwnd_, TTM_POP, 0, 0); 139 } 140 141 void TooltipManagerWin::TooltipTextChanged(View* view) { 142 if (view == last_tooltip_view_) 143 UpdateTooltip(last_mouse_pos_); 144 } 145 146 LRESULT TooltipManagerWin::OnNotify(int w_param, 147 NMHDR* l_param, 148 bool* handled) { 149 *handled = false; 150 if (l_param->hwndFrom == tooltip_hwnd_ && keyboard_tooltip_hwnd_ == NULL) { 151 switch (l_param->code) { 152 case TTN_GETDISPINFO: { 153 if (last_view_out_of_sync_) { 154 // View under the mouse is out of sync, determine it now. 155 View* root_view = widget_->GetRootView(); 156 last_tooltip_view_ = 157 root_view->GetTooltipHandlerForPoint(last_mouse_pos_); 158 last_view_out_of_sync_ = false; 159 } 160 // Tooltip control is asking for the tooltip to display. 161 NMTTDISPINFOW* tooltip_info = 162 reinterpret_cast<NMTTDISPINFOW*>(l_param); 163 // Initialize the string, if we have a valid tooltip the string will 164 // get reset below. 165 tooltip_info->szText[0] = TEXT('\0'); 166 tooltip_text_.clear(); 167 tooltip_info->lpszText = NULL; 168 clipped_text_.clear(); 169 if (last_tooltip_view_ != NULL) { 170 tooltip_text_.clear(); 171 // Mouse is over a View, ask the View for its tooltip. 172 gfx::Point view_loc = last_mouse_pos_; 173 View::ConvertPointToTarget(widget_->GetRootView(), 174 last_tooltip_view_, &view_loc); 175 if (last_tooltip_view_->GetTooltipText(view_loc, &tooltip_text_) && 176 !tooltip_text_.empty()) { 177 // View has a valid tip, copy it into TOOLTIPINFO. 178 clipped_text_ = tooltip_text_; 179 gfx::Point screen_loc = last_mouse_pos_; 180 View::ConvertPointToScreen(widget_->GetRootView(), &screen_loc); 181 TrimTooltipToFit(&clipped_text_, &tooltip_width_, &line_count_, 182 screen_loc.x(), screen_loc.y(), 183 widget_->GetNativeView()); 184 // Adjust the clipped tooltip text for locale direction. 185 base::i18n::AdjustStringForLocaleDirection(&clipped_text_); 186 tooltip_info->lpszText = const_cast<WCHAR*>(clipped_text_.c_str()); 187 } else { 188 tooltip_text_.clear(); 189 } 190 } 191 *handled = true; 192 return 0; 193 } 194 case TTN_POP: 195 tooltip_showing_ = false; 196 *handled = true; 197 return 0; 198 case TTN_SHOW: { 199 *handled = true; 200 tooltip_showing_ = true; 201 // The tooltip is about to show, allow the view to position it 202 gfx::Point text_origin; 203 if (tooltip_height_ == 0) 204 tooltip_height_ = CalcTooltipHeight(); 205 gfx::Point view_loc = last_mouse_pos_; 206 View::ConvertPointToTarget(widget_->GetRootView(), 207 last_tooltip_view_, &view_loc); 208 if (last_tooltip_view_->GetTooltipTextOrigin(view_loc, &text_origin) && 209 SetTooltipPosition(text_origin.x(), text_origin.y())) { 210 // Return true, otherwise the rectangle we specified is ignored. 211 return TRUE; 212 } 213 return 0; 214 } 215 default: 216 // Fall through. 217 break; 218 } 219 } 220 return 0; 221 } 222 223 bool TooltipManagerWin::SetTooltipPosition(int text_x, int text_y) { 224 // NOTE: this really only tests that the y location fits on screen, but that 225 // is good enough for our usage. 226 227 // Calculate the bounds the tooltip will get. 228 gfx::Point view_loc; 229 View::ConvertPointToScreen(last_tooltip_view_, &view_loc); 230 view_loc = ui::win::DIPToScreenPoint(view_loc); 231 RECT bounds = { view_loc.x() + text_x, 232 view_loc.y() + text_y, 233 view_loc.x() + text_x + tooltip_width_, 234 view_loc.y() + line_count_ * GetTooltipHeight() }; 235 SendMessage(tooltip_hwnd_, TTM_ADJUSTRECT, TRUE, (LPARAM)&bounds); 236 237 // Make sure the rectangle completely fits on the current monitor. If it 238 // doesn't, return false so that windows positions the tooltip at the 239 // default location. 240 gfx::Rect monitor_bounds = 241 views::GetMonitorBoundsForRect(gfx::Rect(bounds.left, bounds.right, 242 0, 0)); 243 if (!monitor_bounds.Contains(gfx::Rect(bounds))) { 244 return false; 245 } 246 247 ::SetWindowPos(tooltip_hwnd_, NULL, bounds.left, bounds.top, 0, 0, 248 SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOSIZE); 249 return true; 250 } 251 252 int TooltipManagerWin::CalcTooltipHeight() { 253 // Ask the tooltip for its font. 254 int height; 255 HFONT hfont = reinterpret_cast<HFONT>( 256 SendMessage(tooltip_hwnd_, WM_GETFONT, 0, 0)); 257 if (hfont != NULL) { 258 base::win::ScopedGetDC dc(tooltip_hwnd_); 259 base::win::ScopedSelectObject font(dc, hfont); 260 ui::ScopedSetMapMode mode(dc, MM_TEXT); 261 TEXTMETRIC font_metrics; 262 GetTextMetrics(dc, &font_metrics); 263 height = font_metrics.tmHeight; 264 } else { 265 // Tooltip is using the system font. Use gfx::Font, which should pick 266 // up the system font. 267 height = gfx::Font().GetHeight(); 268 } 269 // Get the margins from the tooltip 270 RECT tooltip_margin; 271 SendMessage(tooltip_hwnd_, TTM_GETMARGIN, 0, (LPARAM)&tooltip_margin); 272 return height + tooltip_margin.top + tooltip_margin.bottom; 273 } 274 275 void TooltipManagerWin::UpdateTooltip(const gfx::Point& mouse_pos) { 276 View* root_view = widget_->GetRootView(); 277 View* view = root_view->GetTooltipHandlerForPoint(mouse_pos); 278 if (view != last_tooltip_view_) { 279 // NOTE: This *must* be sent regardless of the visibility of the tooltip. 280 // It triggers Windows to ask for the tooltip again. 281 SendMessage(tooltip_hwnd_, TTM_POP, 0, 0); 282 last_tooltip_view_ = view; 283 } else if (last_tooltip_view_ != NULL) { 284 // Tooltip is showing, and mouse is over the same view. See if the tooltip 285 // text has changed. 286 gfx::Point view_point = mouse_pos; 287 View::ConvertPointToTarget(root_view, last_tooltip_view_, &view_point); 288 string16 new_tooltip_text; 289 bool has_tooltip_text = 290 last_tooltip_view_->GetTooltipText(view_point, &new_tooltip_text); 291 if (!has_tooltip_text || (new_tooltip_text != tooltip_text_)) { 292 // The text has changed, hide the popup. 293 SendMessage(tooltip_hwnd_, TTM_POP, 0, 0); 294 if (has_tooltip_text && !new_tooltip_text.empty() && tooltip_showing_) { 295 // New text is valid, show the popup. 296 SendMessage(tooltip_hwnd_, TTM_POPUP, 0, 0); 297 } 298 } 299 } 300 } 301 302 void TooltipManagerWin::OnMouse(UINT u_msg, WPARAM w_param, LPARAM l_param) { 303 gfx::Point mouse_pos_in_pixels(l_param); 304 gfx::Point mouse_pos = ui::win::ScreenToDIPPoint(mouse_pos_in_pixels); 305 306 if (u_msg >= WM_NCMOUSEMOVE && u_msg <= WM_NCXBUTTONDBLCLK) { 307 // NC message coordinates are in screen coordinates. 308 POINT temp = mouse_pos_in_pixels.ToPOINT(); 309 ::MapWindowPoints(HWND_DESKTOP, GetParent(), &temp, 1); 310 mouse_pos_in_pixels.SetPoint(temp.x, temp.y); 311 mouse_pos = ui::win::ScreenToDIPPoint(mouse_pos_in_pixels); 312 } 313 314 if (u_msg != WM_MOUSEMOVE || last_mouse_pos_ != mouse_pos) { 315 last_mouse_pos_ = mouse_pos; 316 HideKeyboardTooltip(); 317 UpdateTooltip(mouse_pos); 318 } 319 // Forward the message onto the tooltip. 320 MSG msg; 321 msg.hwnd = GetParent(); 322 msg.message = u_msg; 323 msg.wParam = w_param; 324 msg.lParam = l_param; 325 SendMessage(tooltip_hwnd_, TTM_RELAYEVENT, 0, (LPARAM)&msg); 326 } 327 328 void TooltipManagerWin::ShowKeyboardTooltip(View* focused_view) { 329 if (tooltip_showing_) { 330 SendMessage(tooltip_hwnd_, TTM_POP, 0, 0); 331 tooltip_text_.clear(); 332 } 333 HideKeyboardTooltip(); 334 string16 tooltip_text; 335 if (!focused_view->GetTooltipText(gfx::Point(), &tooltip_text)) 336 return; 337 gfx::Rect focused_bounds = focused_view->bounds(); 338 gfx::Point screen_point; 339 focused_view->ConvertPointToScreen(focused_view, &screen_point); 340 keyboard_tooltip_hwnd_ = CreateWindowEx( 341 WS_EX_TRANSPARENT | l10n_util::GetExtendedTooltipStyles(), 342 TOOLTIPS_CLASS, NULL, 0, 0, 0, 0, 0, NULL, NULL, NULL, NULL); 343 if (!keyboard_tooltip_hwnd_) 344 return; 345 346 SendMessage(keyboard_tooltip_hwnd_, TTM_SETMAXTIPWIDTH, 0, 347 std::numeric_limits<int16>::max()); 348 int tooltip_width; 349 int line_count; 350 TrimTooltipToFit(&tooltip_text, &tooltip_width, &line_count, 351 screen_point.x(), screen_point.y(), 352 widget_->GetNativeView()); 353 ReplaceSubstringsAfterOffset(&tooltip_text, 0, L"\n", L"\r\n"); 354 TOOLINFO keyboard_toolinfo; 355 memset(&keyboard_toolinfo, 0, sizeof(keyboard_toolinfo)); 356 keyboard_toolinfo.cbSize = sizeof(keyboard_toolinfo); 357 keyboard_toolinfo.hwnd = GetParent(); 358 keyboard_toolinfo.uFlags = TTF_TRACK | TTF_TRANSPARENT | TTF_IDISHWND; 359 keyboard_toolinfo.lpszText = const_cast<WCHAR*>(tooltip_text.c_str()); 360 SendMessage(keyboard_tooltip_hwnd_, TTM_ADDTOOL, 0, 361 reinterpret_cast<LPARAM>(&keyboard_toolinfo)); 362 SendMessage(keyboard_tooltip_hwnd_, TTM_TRACKACTIVATE, TRUE, 363 reinterpret_cast<LPARAM>(&keyboard_toolinfo)); 364 if (!tooltip_height_) 365 tooltip_height_ = CalcTooltipHeight(); 366 RECT rect_bounds = {screen_point.x(), 367 screen_point.y() + focused_bounds.height(), 368 screen_point.x() + tooltip_width, 369 screen_point.y() + focused_bounds.height() + 370 line_count * tooltip_height_ }; 371 gfx::Rect monitor_bounds = 372 views::GetMonitorBoundsForRect(gfx::Rect(rect_bounds)); 373 gfx::Rect fitted_bounds = gfx::Rect(rect_bounds); 374 fitted_bounds.AdjustToFit(monitor_bounds); 375 rect_bounds = fitted_bounds.ToRECT(); 376 ::SetWindowPos(keyboard_tooltip_hwnd_, NULL, rect_bounds.left, 377 rect_bounds.top, 0, 0, 378 SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOSIZE); 379 base::MessageLoop::current()->PostDelayedTask( 380 FROM_HERE, 381 base::Bind(&TooltipManagerWin::DestroyKeyboardTooltipWindow, 382 keyboard_tooltip_factory_.GetWeakPtr(), 383 keyboard_tooltip_hwnd_), 384 base::TimeDelta::FromMilliseconds(kDefaultTimeout)); 385 } 386 387 void TooltipManagerWin::HideKeyboardTooltip() { 388 if (keyboard_tooltip_hwnd_ != NULL) { 389 SendMessage(keyboard_tooltip_hwnd_, WM_CLOSE, 0, 0); 390 keyboard_tooltip_hwnd_ = NULL; 391 } 392 } 393 394 void TooltipManagerWin::DestroyKeyboardTooltipWindow(HWND window_to_destroy) { 395 if (keyboard_tooltip_hwnd_ == window_to_destroy) 396 HideKeyboardTooltip(); 397 } 398 399 } // namespace views 400