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