root/chrome/browser/ui/gtk/status_bubble_gtk.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. ignore_next_left_content_
  2. SetStatus
  3. SetURL
  4. SetStatusTextToURL
  5. Show
  6. Hide
  7. SetStatusTextTo
  8. MouseMoved
  9. UpdateDownloadShelfVisibility
  10. Observe
  11. InitWidgets
  12. UserChangedTheme
  13. SetFlipHorizontally
  14. ExpandURL
  15. UpdateLabelSizeRequest
  16. HandleEnterNotify
  17. HandleMotionNotify
  18. AnimationEnded
  19. AnimationProgressed

// 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 "chrome/browser/ui/gtk/status_bubble_gtk.h"

#include <gtk/gtk.h>

#include <algorithm>

#include "base/i18n/rtl.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/elide_url.h"
#include "chrome/browser/ui/gtk/gtk_theme_service.h"
#include "chrome/browser/ui/gtk/gtk_util.h"
#include "chrome/browser/ui/gtk/rounded_window.h"
#include "content/public/browser/notification_source.h"
#include "ui/base/gtk/gtk_hig_constants.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/gtk_compat.h"
#include "ui/gfx/text_elider.h"

namespace {

// Inner padding between the border and the text label.
const int kInternalTopBottomPadding = 1;
const int kInternalLeftRightPadding = 2;

// The radius of the edges of our bubble.
const int kCornerSize = 3;

// Milliseconds before we hide the status bubble widget when you mouseout.
const int kHideDelayMS = 250;

// How close the mouse can get to the infobubble before it starts sliding
// off-screen.
const int kMousePadding = 20;

}  // namespace

StatusBubbleGtk::StatusBubbleGtk(Profile* profile)
    : theme_service_(GtkThemeService::GetFrom(profile)),
      padding_(NULL),
      start_width_(0),
      desired_width_(0),
      flip_horizontally_(false),
      y_offset_(0),
      download_shelf_is_visible_(false),
      last_mouse_left_content_(false),
      ignore_next_left_content_(false) {
  InitWidgets();

  theme_service_->InitThemesFor(this);
  registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
                 content::Source<ThemeService>(theme_service_));
}

StatusBubbleGtk::~StatusBubbleGtk() {
  label_.Destroy();
  container_.Destroy();
}

void StatusBubbleGtk::SetStatus(const base::string16& status_text_wide) {
  std::string status_text = base::UTF16ToUTF8(status_text_wide);
  if (status_text_ == status_text)
    return;

  status_text_ = status_text;
  if (!status_text_.empty())
    SetStatusTextTo(status_text_);
  else if (!url_text_.empty())
    SetStatusTextTo(url_text_);
  else
    SetStatusTextTo(std::string());
}

void StatusBubbleGtk::SetURL(const GURL& url, const std::string& languages) {
  url_ = url;
  languages_ = languages;

  // If we want to clear a displayed URL but there is a status still to
  // display, display that status instead.
  if (url.is_empty() && !status_text_.empty()) {
    url_text_ = std::string();
    SetStatusTextTo(status_text_);
    return;
  }

  SetStatusTextToURL();
}

void StatusBubbleGtk::SetStatusTextToURL() {
  GtkWidget* parent = gtk_widget_get_parent(container_.get());

  // It appears that parent can be NULL (probably only during shutdown).
  if (!parent || !gtk_widget_get_realized(parent))
    return;

  GtkAllocation allocation;
  gtk_widget_get_allocation(parent, &allocation);
  int desired_width = allocation.width;
  if (!expanded()) {
    expand_timer_.Stop();
    expand_timer_.Start(
        FROM_HERE,
        base::TimeDelta::FromMilliseconds(kExpandHoverDelayMS),
        this, &StatusBubbleGtk::ExpandURL);
    // When not expanded, we limit the size to one third the browser's
    // width.
    desired_width /= 3;
  }

  // TODO(tc): We don't actually use gfx::Font as the font in the status
  // bubble.  We should extend ElideUrl to take some sort of pango font.
  url_text_ = base::UTF16ToUTF8(
      ElideUrl(url_, gfx::FontList(), desired_width, languages_));
  SetStatusTextTo(url_text_);
}

void StatusBubbleGtk::Show() {
  // If we were going to hide, stop.
  hide_timer_.Stop();

  gtk_widget_show(container_.get());
  GdkWindow* gdk_window = gtk_widget_get_window(container_.get());
  if (gdk_window)
    gdk_window_raise(gdk_window);
}

void StatusBubbleGtk::Hide() {
  // If we were going to expand the bubble, stop.
  expand_timer_.Stop();
  expand_animation_.reset();

  gtk_widget_hide(container_.get());
}

void StatusBubbleGtk::SetStatusTextTo(const std::string& status_utf8) {
  if (status_utf8.empty()) {
    hide_timer_.Stop();
    hide_timer_.Start(FROM_HERE,
                      base::TimeDelta::FromMilliseconds(kHideDelayMS),
                      this, &StatusBubbleGtk::Hide);
  } else {
    gtk_label_set_text(GTK_LABEL(label_.get()), status_utf8.c_str());
    GtkRequisition req;
    gtk_widget_size_request(label_.get(), &req);
    desired_width_ = req.width;

    UpdateLabelSizeRequest();

    if (!last_mouse_left_content_) {
      // Show the padding and label to update our requisition and then
      // re-process the last mouse event -- if the label was empty before or the
      // text changed, our size will have changed and we may need to move
      // ourselves away from the pointer now.
      gtk_widget_show_all(padding_);
      MouseMoved(last_mouse_location_, false);
    }
    Show();
  }
}

void StatusBubbleGtk::MouseMoved(
    const gfx::Point& location, bool left_content) {
  if (left_content && ignore_next_left_content_) {
    ignore_next_left_content_ = false;
    return;
  }

  last_mouse_location_ = location;
  last_mouse_left_content_ = left_content;

  if (!gtk_widget_get_realized(container_.get()))
    return;

  GtkWidget* parent = gtk_widget_get_parent(container_.get());
  if (!parent || !gtk_widget_get_realized(parent))
    return;

  int old_y_offset = y_offset_;
  bool old_flip_horizontally = flip_horizontally_;

  if (left_content) {
    SetFlipHorizontally(false);
    y_offset_ = 0;
  } else {
    GtkWidget* toplevel = gtk_widget_get_toplevel(container_.get());
    if (!toplevel || !gtk_widget_get_realized(toplevel))
      return;

    bool ltr = !base::i18n::IsRTL();

    GtkRequisition requisition;
    gtk_widget_size_request(container_.get(), &requisition);

    GtkAllocation parent_allocation;
    gtk_widget_get_allocation(parent, &parent_allocation);

    // Get our base position (that is, not including the current offset)
    // relative to the origin of the root window.
    gint toplevel_x = 0, toplevel_y = 0;
    GdkWindow* gdk_window = gtk_widget_get_window(toplevel);
    gdk_window_get_position(gdk_window, &toplevel_x, &toplevel_y);
    gfx::Rect parent_rect =
        gtk_util::GetWidgetRectRelativeToToplevel(parent);
    gfx::Rect bubble_rect(
        toplevel_x + parent_rect.x() +
            (ltr ? 0 : parent_allocation.width - requisition.width),
        toplevel_y + parent_rect.y() +
            parent_allocation.height - requisition.height,
        requisition.width,
        requisition.height);

    int left_threshold =
        bubble_rect.x() - bubble_rect.height() - kMousePadding;
    int right_threshold =
        bubble_rect.right() + bubble_rect.height() + kMousePadding;
    int top_threshold = bubble_rect.y() - kMousePadding;

    if (((ltr && location.x() < right_threshold) ||
         (!ltr && location.x() > left_threshold)) &&
        location.y() > top_threshold) {
      if (download_shelf_is_visible_) {
        SetFlipHorizontally(true);
        y_offset_ = 0;
      } else {
        SetFlipHorizontally(false);
        int distance = std::max(ltr ?
                                    location.x() - right_threshold :
                                    left_threshold - location.x(),
                                top_threshold - location.y());
        y_offset_ = std::min(-1 * distance, requisition.height);
      }
    } else {
      SetFlipHorizontally(false);
      y_offset_ = 0;
    }
  }

  if (y_offset_ != old_y_offset || flip_horizontally_ != old_flip_horizontally)
    gtk_widget_queue_resize_no_redraw(parent);
}

void StatusBubbleGtk::UpdateDownloadShelfVisibility(bool visible) {
  download_shelf_is_visible_ = visible;
}

void StatusBubbleGtk::Observe(int type,
                              const content::NotificationSource& source,
                              const content::NotificationDetails& details) {
  if (type == chrome::NOTIFICATION_BROWSER_THEME_CHANGED) {
    UserChangedTheme();
  }
}

void StatusBubbleGtk::InitWidgets() {
  bool ltr = !base::i18n::IsRTL();

  label_.Own(gtk_label_new(NULL));

  padding_ = gtk_alignment_new(0, 0, 1, 1);
  gtk_alignment_set_padding(GTK_ALIGNMENT(padding_),
      kInternalTopBottomPadding, kInternalTopBottomPadding,
      kInternalLeftRightPadding + (ltr ? 0 : kCornerSize),
      kInternalLeftRightPadding + (ltr ? kCornerSize : 0));
  gtk_container_add(GTK_CONTAINER(padding_), label_.get());
  gtk_widget_show_all(padding_);

  container_.Own(gtk_event_box_new());
  gtk_widget_set_no_show_all(container_.get(), TRUE);
  gtk_util::ActAsRoundedWindow(
      container_.get(), ui::kGdkWhite, kCornerSize,
      gtk_util::ROUNDED_TOP_RIGHT,
      gtk_util::BORDER_TOP | gtk_util::BORDER_RIGHT);
  gtk_widget_set_name(container_.get(), "status-bubble");
  gtk_container_add(GTK_CONTAINER(container_.get()), padding_);

  // We need to listen for mouse motion events, since a fast-moving pointer may
  // enter our window without us getting any motion events on the browser near
  // enough for us to run away.
  gtk_widget_add_events(container_.get(), GDK_POINTER_MOTION_MASK |
                                          GDK_ENTER_NOTIFY_MASK);
  g_signal_connect(container_.get(), "motion-notify-event",
                   G_CALLBACK(HandleMotionNotifyThunk), this);
  g_signal_connect(container_.get(), "enter-notify-event",
                   G_CALLBACK(HandleEnterNotifyThunk), this);

  UserChangedTheme();
}

void StatusBubbleGtk::UserChangedTheme() {
  if (theme_service_->UsingNativeTheme()) {
    gtk_widget_modify_fg(label_.get(), GTK_STATE_NORMAL, NULL);
    gtk_widget_modify_bg(container_.get(), GTK_STATE_NORMAL, NULL);
  } else {
    // TODO(erg): This is the closest to "text that will look good on a
    // toolbar" that I can find. Maybe in later iterations of the theme system,
    // there will be a better color to pick.
    GdkColor bookmark_text =
        theme_service_->GetGdkColor(ThemeProperties::COLOR_BOOKMARK_TEXT);
    gtk_widget_modify_fg(label_.get(), GTK_STATE_NORMAL, &bookmark_text);

    GdkColor toolbar_color =
        theme_service_->GetGdkColor(ThemeProperties::COLOR_TOOLBAR);
    gtk_widget_modify_bg(container_.get(), GTK_STATE_NORMAL, &toolbar_color);
  }

  gtk_util::SetRoundedWindowBorderColor(container_.get(),
                                        theme_service_->GetBorderColor());
}

void StatusBubbleGtk::SetFlipHorizontally(bool flip_horizontally) {
  if (flip_horizontally == flip_horizontally_)
    return;

  flip_horizontally_ = flip_horizontally;

  bool ltr = !base::i18n::IsRTL();
  bool on_left = (ltr && !flip_horizontally) || (!ltr && flip_horizontally);

  gtk_alignment_set_padding(GTK_ALIGNMENT(padding_),
      kInternalTopBottomPadding, kInternalTopBottomPadding,
      kInternalLeftRightPadding + (on_left ? 0 : kCornerSize),
      kInternalLeftRightPadding + (on_left ? kCornerSize : 0));
  // The rounded window code flips these arguments if we're RTL.
  gtk_util::SetRoundedWindowEdgesAndBorders(
      container_.get(),
      kCornerSize,
      flip_horizontally ?
          gtk_util::ROUNDED_TOP_LEFT :
          gtk_util::ROUNDED_TOP_RIGHT,
      gtk_util::BORDER_TOP |
          (flip_horizontally ? gtk_util::BORDER_LEFT : gtk_util::BORDER_RIGHT));
  gtk_widget_queue_draw(container_.get());
}

void StatusBubbleGtk::ExpandURL() {
  GtkAllocation allocation;
  gtk_widget_get_allocation(label_.get(), &allocation);
  start_width_ = allocation.width;
  expand_animation_.reset(new gfx::SlideAnimation(this));
  expand_animation_->SetTweenType(gfx::Tween::LINEAR);
  expand_animation_->Show();

  SetStatusTextToURL();
}

void StatusBubbleGtk::UpdateLabelSizeRequest() {
  if (!expanded() || !expand_animation_->is_animating()) {
    gtk_widget_set_size_request(label_.get(), -1, -1);
    return;
  }

  int new_width = start_width_ +
      (desired_width_ - start_width_) * expand_animation_->GetCurrentValue();
  gtk_widget_set_size_request(label_.get(), new_width, -1);
}

// See http://crbug.com/68897 for why we have to handle this event.
gboolean StatusBubbleGtk::HandleEnterNotify(GtkWidget* sender,
                                            GdkEventCrossing* event) {
  ignore_next_left_content_ = true;
  MouseMoved(gfx::Point(event->x_root, event->y_root), false);
  return FALSE;
}

gboolean StatusBubbleGtk::HandleMotionNotify(GtkWidget* sender,
                                             GdkEventMotion* event) {
  MouseMoved(gfx::Point(event->x_root, event->y_root), false);
  return FALSE;
}

void StatusBubbleGtk::AnimationEnded(const gfx::Animation* animation) {
  UpdateLabelSizeRequest();
}

void StatusBubbleGtk::AnimationProgressed(const gfx::Animation* animation) {
  UpdateLabelSizeRequest();
}

/* [<][>][^][v][top][bottom][index][help] */