Home | History | Annotate | Download | only in corewm
      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/corewm/tooltip_controller.h"
      6 
      7 #include <vector>
      8 
      9 #include "base/command_line.h"
     10 #include "base/location.h"
     11 #include "base/strings/string_split.h"
     12 #include "base/time/time.h"
     13 #include "ui/aura/client/cursor_client.h"
     14 #include "ui/aura/client/drag_drop_client.h"
     15 #include "ui/aura/env.h"
     16 #include "ui/aura/root_window.h"
     17 #include "ui/aura/window.h"
     18 #include "ui/base/events/event.h"
     19 #include "ui/base/resource/resource_bundle.h"
     20 #include "ui/base/text/text_elider.h"
     21 #include "ui/gfx/font.h"
     22 #include "ui/gfx/point.h"
     23 #include "ui/gfx/rect.h"
     24 #include "ui/gfx/screen.h"
     25 #include "ui/views/background.h"
     26 #include "ui/views/border.h"
     27 #include "ui/views/controls/label.h"
     28 #include "ui/views/corewm/corewm_switches.h"
     29 #include "ui/views/widget/widget.h"
     30 #include "ui/views/widget/widget_observer.h"
     31 
     32 namespace {
     33 
     34 const SkColor kTooltipBackground = 0xFFFFFFCC;
     35 const SkColor kTooltipBorder = 0xFF646450;
     36 const int kTooltipBorderWidth = 1;
     37 const int kTooltipHorizontalPadding = 3;
     38 
     39 // Max visual tooltip width. If a tooltip is greater than this width, it will
     40 // be wrapped.
     41 const int kTooltipMaxWidthPixels = 400;
     42 
     43 // Maximum number of lines we allow in the tooltip.
     44 const size_t kMaxLines = 10;
     45 
     46 // TODO(derat): This padding is needed on Chrome OS devices but seems excessive
     47 // when running the same binary on a Linux workstation; presumably there's a
     48 // difference in font metrics.  Rationalize this.
     49 const int kTooltipVerticalPadding = 2;
     50 const int kTooltipTimeoutMs = 500;
     51 const int kDefaultTooltipShownTimeoutMs = 10000;
     52 
     53 // FIXME: get cursor offset from actual cursor size.
     54 const int kCursorOffsetX = 10;
     55 const int kCursorOffsetY = 15;
     56 
     57 // Maximum number of characters we allow in a tooltip.
     58 const size_t kMaxTooltipLength = 1024;
     59 
     60 gfx::Font GetDefaultFont() {
     61   // TODO(varunjain): implementation duplicated in tooltip_manager_aura. Figure
     62   // out a way to merge.
     63   return ui::ResourceBundle::GetSharedInstance().GetFont(
     64       ui::ResourceBundle::BaseFont);
     65 }
     66 
     67 // Creates a widget of type TYPE_TOOLTIP
     68 views::Widget* CreateTooltip(aura::Window* tooltip_window) {
     69   views::Widget* widget = new views::Widget;
     70   views::Widget::InitParams params;
     71   // For aura, since we set the type to TOOLTIP_TYPE, the widget will get
     72   // auto-parented to the MenuAndTooltipsContainer.
     73   params.type = views::Widget::InitParams::TYPE_TOOLTIP;
     74   params.context = tooltip_window;
     75   DCHECK(params.context);
     76   params.keep_on_top = true;
     77   params.accept_events = false;
     78   widget->Init(params);
     79   return widget;
     80 }
     81 
     82 }  // namespace
     83 
     84 namespace views {
     85 namespace corewm {
     86 
     87 // Displays a widget with tooltip using a views::Label.
     88 class TooltipController::Tooltip : public views::WidgetObserver {
     89  public:
     90   Tooltip(TooltipController* controller)
     91       : controller_(controller),
     92         widget_(NULL) {
     93     label_.set_background(
     94         views::Background::CreateSolidBackground(kTooltipBackground));
     95     if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kNoDropShadows)) {
     96       label_.set_border(
     97           views::Border::CreateSolidBorder(kTooltipBorderWidth,
     98                                            kTooltipBorder));
     99     }
    100     label_.set_owned_by_client();
    101     label_.SetMultiLine(true);
    102   }
    103 
    104   virtual ~Tooltip() {
    105     if (widget_) {
    106       widget_->RemoveObserver(this);
    107       widget_->Close();
    108     }
    109   }
    110 
    111   // Updates the text on the tooltip and resizes to fit.
    112   void SetText(aura::Window* window,
    113                const string16& tooltip_text,
    114                const gfx::Point& location) {
    115     int max_width, line_count;
    116     string16 trimmed_text(tooltip_text);
    117     controller_->TrimTooltipToFit(
    118         controller_->GetMaxWidth(location), &trimmed_text, &max_width,
    119         &line_count);
    120     label_.SetText(trimmed_text);
    121 
    122     int width = max_width + 2 * kTooltipHorizontalPadding;
    123     int height = label_.GetHeightForWidth(max_width) +
    124         2 * kTooltipVerticalPadding;
    125     if (CommandLine::ForCurrentProcess()->HasSwitch(switches::kNoDropShadows)) {
    126       width += 2 * kTooltipBorderWidth;
    127       height += 2 * kTooltipBorderWidth;
    128     }
    129     CreateWidgetIfNecessary(window);
    130     SetTooltipBounds(location, width, height);
    131   }
    132 
    133   // Shows the tooltip.
    134   void Show() {
    135     if (widget_)
    136       widget_->Show();
    137   }
    138 
    139   // Hides the tooltip.
    140   void Hide() {
    141     if (widget_)
    142       widget_->Hide();
    143   }
    144 
    145   bool IsVisible() {
    146     return widget_ && widget_->IsVisible();
    147   }
    148 
    149   // Overriden from views::WidgetObserver.
    150   virtual void OnWidgetDestroying(views::Widget* widget) OVERRIDE {
    151     DCHECK_EQ(widget_, widget);
    152     widget_ = NULL;
    153   }
    154 
    155  private:
    156   // Adjusts the bounds given by the arguments to fit inside the desktop
    157   // and applies the adjusted bounds to the label_.
    158   void SetTooltipBounds(const gfx::Point& mouse_pos,
    159                         int tooltip_width,
    160                         int tooltip_height) {
    161     gfx::Rect tooltip_rect(mouse_pos.x(), mouse_pos.y(), tooltip_width,
    162                            tooltip_height);
    163 
    164     tooltip_rect.Offset(kCursorOffsetX, kCursorOffsetY);
    165     gfx::Rect display_bounds = controller_->GetBoundsForTooltip(mouse_pos);
    166 
    167     // If tooltip is out of bounds on the x axis, we simply shift it
    168     // horizontally by the offset.
    169     if (tooltip_rect.right() > display_bounds.right()) {
    170       int h_offset = tooltip_rect.right() - display_bounds.right();
    171       tooltip_rect.Offset(-h_offset, 0);
    172     }
    173 
    174     // If tooltip is out of bounds on the y axis, we flip it to appear above the
    175     // mouse cursor instead of below.
    176     if (tooltip_rect.bottom() > display_bounds.bottom())
    177       tooltip_rect.set_y(mouse_pos.y() - tooltip_height);
    178 
    179     tooltip_rect.AdjustToFit(display_bounds);
    180     widget_->SetBounds(tooltip_rect);
    181   }
    182 
    183   void CreateWidgetIfNecessary(aura::Window* tooltip_window) {
    184     if (widget_)
    185       return;
    186     widget_ = CreateTooltip(tooltip_window);
    187     widget_->SetContentsView(&label_);
    188     widget_->AddObserver(this);
    189   }
    190 
    191   views::Label label_;
    192   TooltipController* controller_;
    193   views::Widget* widget_;
    194 
    195   DISALLOW_COPY_AND_ASSIGN(Tooltip);
    196 };
    197 
    198 ////////////////////////////////////////////////////////////////////////////////
    199 // TooltipController public:
    200 
    201 TooltipController::TooltipController(gfx::ScreenType screen_type)
    202     : screen_type_(screen_type),
    203       tooltip_window_(NULL),
    204       tooltip_window_at_mouse_press_(NULL),
    205       mouse_pressed_(false),
    206       tooltips_enabled_(true) {
    207   tooltip_timer_.Start(FROM_HERE,
    208       base::TimeDelta::FromMilliseconds(kTooltipTimeoutMs),
    209       this, &TooltipController::TooltipTimerFired);
    210 }
    211 
    212 TooltipController::~TooltipController() {
    213   if (tooltip_window_)
    214     tooltip_window_->RemoveObserver(this);
    215 }
    216 
    217 void TooltipController::UpdateTooltip(aura::Window* target) {
    218   // If tooltip is visible, we may want to hide it. If it is not, we are ok.
    219   if (tooltip_window_ == target && GetTooltip()->IsVisible())
    220     UpdateIfRequired();
    221 
    222   // If we had stopped the tooltip timer for some reason, we must restart it if
    223   // there is a change in the tooltip.
    224   if (!tooltip_timer_.IsRunning()) {
    225     if (tooltip_window_ != target || (tooltip_window_ &&
    226         tooltip_text_ != aura::client::GetTooltipText(tooltip_window_))) {
    227       tooltip_timer_.Start(FROM_HERE,
    228           base::TimeDelta::FromMilliseconds(kTooltipTimeoutMs),
    229           this, &TooltipController::TooltipTimerFired);
    230     }
    231   }
    232 }
    233 
    234 void TooltipController::SetTooltipShownTimeout(aura::Window* target,
    235                                                int timeout_in_ms) {
    236   tooltip_shown_timeout_map_[target] = timeout_in_ms;
    237 }
    238 
    239 void TooltipController::SetTooltipsEnabled(bool enable) {
    240   if (tooltips_enabled_ == enable)
    241     return;
    242   tooltips_enabled_ = enable;
    243   UpdateTooltip(tooltip_window_);
    244 }
    245 
    246 void TooltipController::OnKeyEvent(ui::KeyEvent* event) {
    247   // On key press, we want to hide the tooltip and not show it until change.
    248   // This is the same behavior as hiding tooltips on timeout. Hence, we can
    249   // simply simulate a timeout.
    250   if (tooltip_shown_timer_.IsRunning()) {
    251     tooltip_shown_timer_.Stop();
    252     TooltipShownTimerFired();
    253   }
    254 }
    255 
    256 void TooltipController::OnMouseEvent(ui::MouseEvent* event) {
    257   aura::Window* target = static_cast<aura::Window*>(event->target());
    258   switch (event->type()) {
    259     case ui::ET_MOUSE_MOVED:
    260     case ui::ET_MOUSE_DRAGGED:
    261       if (tooltip_window_ != target) {
    262         if (tooltip_window_)
    263           tooltip_window_->RemoveObserver(this);
    264         tooltip_window_ = target;
    265         tooltip_window_->AddObserver(this);
    266       }
    267       curr_mouse_loc_ = event->location();
    268       if (tooltip_timer_.IsRunning())
    269         tooltip_timer_.Reset();
    270 
    271       if (GetTooltip()->IsVisible())
    272         UpdateIfRequired();
    273       break;
    274     case ui::ET_MOUSE_PRESSED:
    275       if ((event->flags() & ui::EF_IS_NON_CLIENT) == 0) {
    276         // We don't get a release for non-client areas.
    277         mouse_pressed_ = true;
    278         tooltip_window_at_mouse_press_ = target;
    279         if (target)
    280           tooltip_text_at_mouse_press_ = aura::client::GetTooltipText(target);
    281       }
    282       GetTooltip()->Hide();
    283       break;
    284     case ui::ET_MOUSE_RELEASED:
    285       mouse_pressed_ = false;
    286       break;
    287     case ui::ET_MOUSE_CAPTURE_CHANGED:
    288       // We will not received a mouse release, so reset mouse pressed state.
    289       mouse_pressed_ = false;
    290     case ui::ET_MOUSEWHEEL:
    291       // Hide the tooltip for click, release, drag, wheel events.
    292       if (GetTooltip()->IsVisible())
    293         GetTooltip()->Hide();
    294       break;
    295     default:
    296       break;
    297   }
    298 }
    299 
    300 void TooltipController::OnTouchEvent(ui::TouchEvent* event) {
    301   // TODO(varunjain): need to properly implement tooltips for
    302   // touch events.
    303   // Hide the tooltip for touch events.
    304   if (GetTooltip()->IsVisible())
    305     GetTooltip()->Hide();
    306   if (tooltip_window_)
    307     tooltip_window_->RemoveObserver(this);
    308   tooltip_window_ = NULL;
    309 }
    310 
    311 void TooltipController::OnCancelMode(ui::CancelModeEvent* event) {
    312   if (tooltip_.get() && tooltip_->IsVisible())
    313     tooltip_->Hide();
    314 }
    315 
    316 void TooltipController::OnWindowDestroyed(aura::Window* window) {
    317   if (tooltip_window_ == window) {
    318     tooltip_shown_timeout_map_.erase(tooltip_window_);
    319     tooltip_window_->RemoveObserver(this);
    320     tooltip_window_ = NULL;
    321   }
    322 }
    323 
    324 ////////////////////////////////////////////////////////////////////////////////
    325 // TooltipController private:
    326 
    327 int TooltipController::GetMaxWidth(const gfx::Point& location) const {
    328   // TODO(varunjain): implementation duplicated in tooltip_manager_aura. Figure
    329   // out a way to merge.
    330   gfx::Rect display_bounds = GetBoundsForTooltip(location);
    331   return (display_bounds.width() + 1) / 2;
    332 }
    333 
    334 gfx::Rect TooltipController::GetBoundsForTooltip(
    335     const gfx::Point& origin) const {
    336   DCHECK(tooltip_window_);
    337   gfx::Rect widget_bounds;
    338   // For Desktop aura we constrain the tooltip to the bounds of the Widget
    339   // (which comes from the RootWindow).
    340   if (screen_type_ == gfx::SCREEN_TYPE_NATIVE &&
    341       gfx::SCREEN_TYPE_NATIVE != gfx::SCREEN_TYPE_ALTERNATE) {
    342     aura::RootWindow* root = tooltip_window_->GetRootWindow();
    343     widget_bounds = gfx::Rect(root->GetHostOrigin(), root->GetHostSize());
    344   }
    345   gfx::Screen* screen = gfx::Screen::GetScreenByType(screen_type_);
    346   gfx::Rect bounds(screen->GetDisplayNearestPoint(origin).bounds());
    347   if (!widget_bounds.IsEmpty())
    348     bounds.Intersect(widget_bounds);
    349   return bounds;
    350 }
    351 
    352 // static
    353 void TooltipController::TrimTooltipToFit(int max_width,
    354                                          string16* text,
    355                                          int* width,
    356                                          int* line_count) {
    357   *width = 0;
    358   *line_count = 0;
    359 
    360   // Clamp the tooltip length to kMaxTooltipLength so that we don't
    361   // accidentally DOS the user with a mega tooltip.
    362   if (text->length() > kMaxTooltipLength)
    363     *text = text->substr(0, kMaxTooltipLength);
    364 
    365   // Determine the available width for the tooltip.
    366   int available_width = std::min(kTooltipMaxWidthPixels, max_width);
    367 
    368   std::vector<string16> lines;
    369   base::SplitString(*text, '\n', &lines);
    370   std::vector<string16> result_lines;
    371 
    372   // Format each line to fit.
    373   gfx::Font font = GetDefaultFont();
    374   for (std::vector<string16>::iterator l = lines.begin(); l != lines.end();
    375       ++l) {
    376     // We break the line at word boundaries, then stuff as many words as we can
    377     // in the available width to the current line, and move the remaining words
    378     // to a new line.
    379     std::vector<string16> words;
    380     base::SplitStringDontTrim(*l, ' ', &words);
    381     int current_width = 0;
    382     string16 line;
    383     for (std::vector<string16>::iterator w = words.begin(); w != words.end();
    384         ++w) {
    385       string16 word = *w;
    386       if (w + 1 != words.end())
    387         word.push_back(' ');
    388       int word_width = font.GetStringWidth(word);
    389       if (current_width + word_width > available_width) {
    390         // Current width will exceed the available width. Must start a new line.
    391         if (!line.empty())
    392           result_lines.push_back(line);
    393         current_width = 0;
    394         line.clear();
    395       }
    396       current_width += word_width;
    397       line.append(word);
    398     }
    399     result_lines.push_back(line);
    400   }
    401 
    402   // Clamp number of lines to |kMaxLines|.
    403   if (result_lines.size() > kMaxLines) {
    404     result_lines.resize(kMaxLines);
    405     // Add ellipses character to last line.
    406     result_lines[kMaxLines - 1] = ui::TruncateString(
    407         result_lines.back(), result_lines.back().length() - 1);
    408   }
    409   *line_count = result_lines.size();
    410 
    411   // Flatten the result.
    412   string16 result;
    413   for (std::vector<string16>::iterator l = result_lines.begin();
    414       l != result_lines.end(); ++l) {
    415     if (!result.empty())
    416       result.push_back('\n');
    417     int line_width = font.GetStringWidth(*l);
    418     // Since we only break at word boundaries, it could happen that due to some
    419     // very long word, line_width is greater than the available_width. In such
    420     // case, we simply truncate at available_width and add ellipses at the end.
    421     if (line_width > available_width) {
    422       *width = available_width;
    423       result.append(ui::ElideText(*l, font, available_width, ui::ELIDE_AT_END));
    424     } else {
    425       *width = std::max(*width, line_width);
    426       result.append(*l);
    427     }
    428   }
    429   *text = result;
    430 }
    431 
    432 void TooltipController::TooltipTimerFired() {
    433   UpdateIfRequired();
    434 }
    435 
    436 void TooltipController::TooltipShownTimerFired() {
    437   GetTooltip()->Hide();
    438 
    439   // Since the user presumably no longer needs the tooltip, we also stop the
    440   // tooltip timer so that tooltip does not pop back up. We will restart this
    441   // timer if the tooltip changes (see UpdateTooltip()).
    442   tooltip_timer_.Stop();
    443 }
    444 
    445 void TooltipController::UpdateIfRequired() {
    446   if (!tooltips_enabled_ || mouse_pressed_ || IsDragDropInProgress() ||
    447       !IsCursorVisible()) {
    448     GetTooltip()->Hide();
    449     return;
    450   }
    451 
    452   string16 tooltip_text;
    453   if (tooltip_window_)
    454     tooltip_text = aura::client::GetTooltipText(tooltip_window_);
    455 
    456   // If the user pressed a mouse button. We will hide the tooltip and not show
    457   // it until there is a change in the tooltip.
    458   if (tooltip_window_at_mouse_press_) {
    459     if (tooltip_window_ == tooltip_window_at_mouse_press_ &&
    460         tooltip_text == tooltip_text_at_mouse_press_) {
    461       GetTooltip()->Hide();
    462       return;
    463     }
    464     tooltip_window_at_mouse_press_ = NULL;
    465   }
    466 
    467   // We add the !GetTooltip()->IsVisible() below because when we come here from
    468   // TooltipTimerFired(), the tooltip_text may not have changed but we still
    469   // want to update the tooltip because the timer has fired.
    470   // If we come here from UpdateTooltip(), we have already checked for tooltip
    471   // visibility and this check below will have no effect.
    472   if (tooltip_text_ != tooltip_text || !GetTooltip()->IsVisible()) {
    473     tooltip_shown_timer_.Stop();
    474     tooltip_text_ = tooltip_text;
    475     if (tooltip_text_.empty()) {
    476       GetTooltip()->Hide();
    477     } else {
    478       gfx::Point widget_loc = curr_mouse_loc_ +
    479           tooltip_window_->GetBoundsInScreen().OffsetFromOrigin();
    480       gfx::Rect bounds(GetBoundsForTooltip(widget_loc));
    481       if (bounds.IsEmpty()) {
    482         tooltip_text_.clear();
    483         GetTooltip()->Hide();
    484       } else {
    485         GetTooltip()->SetText(tooltip_window_, tooltip_text_, widget_loc);
    486         GetTooltip()->Show();
    487         int timeout = GetTooltipShownTimeout();
    488         if (timeout > 0) {
    489           tooltip_shown_timer_.Start(FROM_HERE,
    490                 base::TimeDelta::FromMilliseconds(timeout),
    491                 this, &TooltipController::TooltipShownTimerFired);
    492         }
    493       }
    494     }
    495   }
    496 }
    497 
    498 bool TooltipController::IsTooltipVisible() {
    499   return GetTooltip()->IsVisible();
    500 }
    501 
    502 bool TooltipController::IsDragDropInProgress() {
    503   if (!tooltip_window_)
    504     return false;
    505   aura::client::DragDropClient* client =
    506       aura::client::GetDragDropClient(tooltip_window_->GetRootWindow());
    507   return client && client->IsDragDropInProgress();
    508 }
    509 
    510 TooltipController::Tooltip* TooltipController::GetTooltip() {
    511   if (!tooltip_.get())
    512     tooltip_.reset(new Tooltip(this));
    513   return tooltip_.get();
    514 }
    515 
    516 bool TooltipController::IsCursorVisible() {
    517   if (!tooltip_window_)
    518     return false;
    519   aura::RootWindow* root = tooltip_window_->GetRootWindow();
    520   if (!root)
    521     return false;
    522   aura::client::CursorClient* cursor_client =
    523       aura::client::GetCursorClient(root);
    524   // |cursor_client| may be NULL in tests, treat NULL as always visible.
    525   return !cursor_client || cursor_client->IsCursorVisible();
    526 }
    527 
    528 int TooltipController::GetTooltipShownTimeout() {
    529   std::map<aura::Window*, int>::const_iterator it =
    530       tooltip_shown_timeout_map_.find(tooltip_window_);
    531   if (it == tooltip_shown_timeout_map_.end())
    532     return kDefaultTooltipShownTimeoutMs;
    533   return it->second;
    534 }
    535 
    536 }  // namespace corewm
    537 }  // namespace views
    538