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 
     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