Home | History | Annotate | Download | only in widget
      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