| // 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/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.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 kHideDelay = 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 string16& status_text_wide) { |
| std::string status_text = 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(kExpandHoverDelay), |
| 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 gfx::ElideUrl to take some sort of pango font. |
| url_text_ = UTF16ToUTF8( |
| gfx::ElideUrl(url_, gfx::Font(), 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(kHideDelay), |
| 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(); |
| } |