// 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/widget/tooltip_manager_win.h" #include <windowsx.h> #include <limits> #include <vector> #include "base/bind.h" #include "base/i18n/rtl.h" #include "base/logging.h" #include "base/message_loop/message_loop.h" #include "base/strings/string_split.h" #include "base/strings/string_util.h" #include "base/win/scoped_hdc.h" #include "base/win/scoped_select_object.h" #include "ui/base/l10n/l10n_util_win.h" #include "ui/gfx/font_list.h" #include "ui/gfx/screen.h" #include "ui/gfx/text_elider.h" #include "ui/gfx/text_utils.h" #include "ui/gfx/win/dpi.h" #include "ui/gfx/win/hwnd_util.h" #include "ui/gfx/win/scoped_set_map_mode.h" #include "ui/views/view.h" #include "ui/views/widget/monitor_win.h" #include "ui/views/widget/widget.h" namespace views { namespace { static int tooltip_height_ = 0; // Maximum number of lines we allow in the tooltip. const size_t kMaxLines = 6; // Trims the tooltip to fit, setting |text| to the clipped result, |max_width| // to the width (in pixels) of the clipped text and |line_count| to the number // of lines of text in the tooltip. |available_width| gives the space available // for the tooltip. void TrimTooltipToFit(const gfx::FontList& font_list, int available_width, base::string16* text, int* max_width, int* line_count) { *max_width = 0; *line_count = 0; TooltipManager::TrimTooltipText(text); // Split the string into at most kMaxLines lines. std::vector<base::string16> lines; base::SplitString(*text, '\n', &lines); if (lines.size() > kMaxLines) lines.resize(kMaxLines); *line_count = static_cast<int>(lines.size()); // Format each line to fit. base::string16 result; for (std::vector<base::string16>::iterator i = lines.begin(); i != lines.end(); ++i) { base::string16 elided_text = gfx::ElideText(*i, font_list, available_width, gfx::ELIDE_AT_END); *max_width = std::max(*max_width, gfx::GetStringWidth(elided_text, font_list)); if (!result.empty()) result.push_back('\n'); result.append(elided_text); } *text = result; } } // namespace // static int TooltipManager::GetTooltipHeight() { DCHECK_GT(tooltip_height_, 0); return tooltip_height_; } static gfx::Font DetermineDefaultFont() { HWND window = CreateWindowEx( WS_EX_TRANSPARENT | l10n_util::GetExtendedTooltipStyles(), TOOLTIPS_CLASS, NULL, 0 , 0, 0, 0, 0, NULL, NULL, NULL, NULL); if (!window) return gfx::Font(); HFONT hfont = reinterpret_cast<HFONT>(SendMessage(window, WM_GETFONT, 0, 0)); gfx::Font font = hfont ? gfx::Font(hfont) : gfx::Font(); DestroyWindow(window); return font; } TooltipManagerWin::TooltipManagerWin(Widget* widget) : widget_(widget), tooltip_hwnd_(NULL), last_mouse_pos_(-1, -1), tooltip_showing_(false), last_tooltip_view_(NULL), last_view_out_of_sync_(false), tooltip_width_(0) { DCHECK(widget); DCHECK(widget->GetNativeView()); } TooltipManagerWin::~TooltipManagerWin() { if (tooltip_hwnd_) DestroyWindow(tooltip_hwnd_); } bool TooltipManagerWin::Init() { DCHECK(!tooltip_hwnd_); // Create the tooltip control. tooltip_hwnd_ = CreateWindowEx( WS_EX_TRANSPARENT | l10n_util::GetExtendedTooltipStyles(), TOOLTIPS_CLASS, NULL, TTS_NOPREFIX, 0, 0, 0, 0, GetParent(), NULL, NULL, NULL); if (!tooltip_hwnd_) return false; l10n_util::AdjustUIFontForWindow(tooltip_hwnd_); // This effectively turns off clipping of tooltips. We need this otherwise // multi-line text (\r\n) won't work right. The size doesn't really matter // (just as long as its bigger than the monitor's width) as we clip to the // screen size before rendering. SendMessage(tooltip_hwnd_, TTM_SETMAXTIPWIDTH, 0, std::numeric_limits<int16>::max()); // Add one tool that is used for all tooltips. toolinfo_.cbSize = sizeof(toolinfo_); toolinfo_.uFlags = TTF_TRANSPARENT | TTF_IDISHWND; toolinfo_.hwnd = GetParent(); toolinfo_.uId = reinterpret_cast<UINT_PTR>(GetParent()); // Setting this tells windows to call GetParent() back (using a WM_NOTIFY // message) for the actual tooltip contents. toolinfo_.lpszText = LPSTR_TEXTCALLBACK; toolinfo_.lpReserved = NULL; SetRectEmpty(&toolinfo_.rect); SendMessage(tooltip_hwnd_, TTM_ADDTOOL, 0, (LPARAM)&toolinfo_); return true; } gfx::NativeView TooltipManagerWin::GetParent() { return widget_->GetNativeView(); } const gfx::FontList& TooltipManagerWin::GetFontList() const { static gfx::FontList* font_list = NULL; if (!font_list) font_list = new gfx::FontList(DetermineDefaultFont()); return *font_list; } void TooltipManagerWin::UpdateTooltip() { // Set last_view_out_of_sync_ to indicate the view is currently out of sync. // This doesn't update the view under the mouse immediately as it may cause // timing problems. last_view_out_of_sync_ = true; last_tooltip_view_ = NULL; // Hide the tooltip. SendMessage(tooltip_hwnd_, TTM_POP, 0, 0); } void TooltipManagerWin::TooltipTextChanged(View* view) { if (view == last_tooltip_view_) UpdateTooltip(last_mouse_pos_); } LRESULT TooltipManagerWin::OnNotify(int w_param, NMHDR* l_param, bool* handled) { *handled = false; if (l_param->hwndFrom != tooltip_hwnd_) return 0; switch (l_param->code) { case TTN_GETDISPINFO: { if (last_view_out_of_sync_) { // View under the mouse is out of sync, determine it now. View* root_view = widget_->GetRootView(); last_tooltip_view_ = root_view->GetTooltipHandlerForPoint(last_mouse_pos_); last_view_out_of_sync_ = false; } // Tooltip control is asking for the tooltip to display. NMTTDISPINFOW* tooltip_info = reinterpret_cast<NMTTDISPINFOW*>(l_param); // Initialize the string, if we have a valid tooltip the string will // get reset below. tooltip_info->szText[0] = TEXT('\0'); tooltip_text_.clear(); tooltip_info->lpszText = NULL; clipped_text_.clear(); if (last_tooltip_view_ != NULL) { tooltip_text_.clear(); // Mouse is over a View, ask the View for its tooltip. gfx::Point view_loc = last_mouse_pos_; View::ConvertPointToTarget(widget_->GetRootView(), last_tooltip_view_, &view_loc); if (last_tooltip_view_->GetTooltipText(view_loc, &tooltip_text_) && !tooltip_text_.empty()) { // View has a valid tip, copy it into TOOLTIPINFO. clipped_text_ = tooltip_text_; gfx::Point screen_loc = last_mouse_pos_; View::ConvertPointToScreen(widget_->GetRootView(), &screen_loc); TrimTooltipToFit( GetFontList(), GetMaxWidth(screen_loc.x(), screen_loc.y(), widget_->GetNativeView()), &clipped_text_, &tooltip_width_, &line_count_); // Adjust the clipped tooltip text for locale direction. base::i18n::AdjustStringForLocaleDirection(&clipped_text_); tooltip_info->lpszText = const_cast<WCHAR*>(clipped_text_.c_str()); } else { tooltip_text_.clear(); } } *handled = true; return 0; } case TTN_POP: tooltip_showing_ = false; *handled = true; return 0; case TTN_SHOW: { *handled = true; tooltip_showing_ = true; // The tooltip is about to show, allow the view to position it gfx::Point text_origin; if (tooltip_height_ == 0) tooltip_height_ = CalcTooltipHeight(); gfx::Point view_loc = last_mouse_pos_; View::ConvertPointToTarget(widget_->GetRootView(), last_tooltip_view_, &view_loc); if (last_tooltip_view_->GetTooltipTextOrigin(view_loc, &text_origin) && SetTooltipPosition(text_origin.x(), text_origin.y())) { // Return true, otherwise the rectangle we specified is ignored. return TRUE; } return 0; } default: // Fall through. break; } return 0; } bool TooltipManagerWin::SetTooltipPosition(int text_x, int text_y) { // NOTE: this really only tests that the y location fits on screen, but that // is good enough for our usage. // Calculate the bounds the tooltip will get. gfx::Point view_loc; View::ConvertPointToScreen(last_tooltip_view_, &view_loc); view_loc = gfx::win::DIPToScreenPoint(view_loc); RECT bounds = { view_loc.x() + text_x, view_loc.y() + text_y, view_loc.x() + text_x + tooltip_width_, view_loc.y() + line_count_ * GetTooltipHeight() }; SendMessage(tooltip_hwnd_, TTM_ADJUSTRECT, TRUE, (LPARAM)&bounds); // Make sure the rectangle completely fits on the current monitor. If it // doesn't, return false so that windows positions the tooltip at the // default location. gfx::Rect monitor_bounds = views::GetMonitorBoundsForRect(gfx::Rect(bounds.left, bounds.right, 0, 0)); if (!monitor_bounds.Contains(gfx::Rect(bounds))) { return false; } ::SetWindowPos(tooltip_hwnd_, NULL, bounds.left, bounds.top, 0, 0, SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOSIZE); return true; } int TooltipManagerWin::CalcTooltipHeight() { // Ask the tooltip for its font. int height; HFONT hfont = reinterpret_cast<HFONT>( SendMessage(tooltip_hwnd_, WM_GETFONT, 0, 0)); if (hfont != NULL) { base::win::ScopedGetDC dc(tooltip_hwnd_); base::win::ScopedSelectObject font(dc, hfont); gfx::ScopedSetMapMode mode(dc, MM_TEXT); TEXTMETRIC font_metrics; GetTextMetrics(dc, &font_metrics); height = font_metrics.tmHeight; } else { // Tooltip is using the system font. Use gfx::Font, which should pick // up the system font. height = gfx::Font().GetHeight(); } // Get the margins from the tooltip RECT tooltip_margin; SendMessage(tooltip_hwnd_, TTM_GETMARGIN, 0, (LPARAM)&tooltip_margin); return height + tooltip_margin.top + tooltip_margin.bottom; } void TooltipManagerWin::UpdateTooltip(const gfx::Point& mouse_pos) { View* root_view = widget_->GetRootView(); View* view = root_view->GetTooltipHandlerForPoint(mouse_pos); if (view != last_tooltip_view_) { // NOTE: This *must* be sent regardless of the visibility of the tooltip. // It triggers Windows to ask for the tooltip again. SendMessage(tooltip_hwnd_, TTM_POP, 0, 0); last_tooltip_view_ = view; } else if (last_tooltip_view_ != NULL) { // Tooltip is showing, and mouse is over the same view. See if the tooltip // text has changed. gfx::Point view_point = mouse_pos; View::ConvertPointToTarget(root_view, last_tooltip_view_, &view_point); string16 new_tooltip_text; bool has_tooltip_text = last_tooltip_view_->GetTooltipText(view_point, &new_tooltip_text); if (!has_tooltip_text || (new_tooltip_text != tooltip_text_)) { // The text has changed, hide the popup. SendMessage(tooltip_hwnd_, TTM_POP, 0, 0); if (has_tooltip_text && !new_tooltip_text.empty() && tooltip_showing_) { // New text is valid, show the popup. SendMessage(tooltip_hwnd_, TTM_POPUP, 0, 0); } } } } void TooltipManagerWin::OnMouse(UINT u_msg, WPARAM w_param, LPARAM l_param) { gfx::Point mouse_pos_in_pixels(l_param); gfx::Point mouse_pos = gfx::win::ScreenToDIPPoint(mouse_pos_in_pixels); if (u_msg >= WM_NCMOUSEMOVE && u_msg <= WM_NCXBUTTONDBLCLK) { // NC message coordinates are in screen coordinates. POINT temp = mouse_pos_in_pixels.ToPOINT(); ::MapWindowPoints(HWND_DESKTOP, GetParent(), &temp, 1); mouse_pos_in_pixels.SetPoint(temp.x, temp.y); mouse_pos = gfx::win::ScreenToDIPPoint(mouse_pos_in_pixels); } if (u_msg != WM_MOUSEMOVE || last_mouse_pos_ != mouse_pos) { last_mouse_pos_ = mouse_pos; UpdateTooltip(mouse_pos); } // Forward the message onto the tooltip. MSG msg; msg.hwnd = GetParent(); msg.message = u_msg; msg.wParam = w_param; msg.lParam = l_param; SendMessage(tooltip_hwnd_, TTM_RELAYEVENT, 0, (LPARAM)&msg); } } // namespace views