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 "chrome/browser/ui/gtk/status_bubble_gtk.h" 6 7 #include <gtk/gtk.h> 8 9 #include <algorithm> 10 11 #include "base/i18n/rtl.h" 12 #include "base/message_loop/message_loop.h" 13 #include "base/strings/utf_string_conversions.h" 14 #include "chrome/browser/chrome_notification_types.h" 15 #include "chrome/browser/themes/theme_properties.h" 16 #include "chrome/browser/ui/gtk/gtk_theme_service.h" 17 #include "chrome/browser/ui/gtk/gtk_util.h" 18 #include "chrome/browser/ui/gtk/rounded_window.h" 19 #include "content/public/browser/notification_source.h" 20 #include "ui/base/gtk/gtk_hig_constants.h" 21 #include "ui/gfx/animation/slide_animation.h" 22 #include "ui/gfx/font_list.h" 23 #include "ui/gfx/gtk_compat.h" 24 #include "ui/gfx/text_elider.h" 25 26 namespace { 27 28 // Inner padding between the border and the text label. 29 const int kInternalTopBottomPadding = 1; 30 const int kInternalLeftRightPadding = 2; 31 32 // The radius of the edges of our bubble. 33 const int kCornerSize = 3; 34 35 // Milliseconds before we hide the status bubble widget when you mouseout. 36 const int kHideDelay = 250; 37 38 // How close the mouse can get to the infobubble before it starts sliding 39 // off-screen. 40 const int kMousePadding = 20; 41 42 } // namespace 43 44 StatusBubbleGtk::StatusBubbleGtk(Profile* profile) 45 : theme_service_(GtkThemeService::GetFrom(profile)), 46 padding_(NULL), 47 start_width_(0), 48 desired_width_(0), 49 flip_horizontally_(false), 50 y_offset_(0), 51 download_shelf_is_visible_(false), 52 last_mouse_left_content_(false), 53 ignore_next_left_content_(false) { 54 InitWidgets(); 55 56 theme_service_->InitThemesFor(this); 57 registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED, 58 content::Source<ThemeService>(theme_service_)); 59 } 60 61 StatusBubbleGtk::~StatusBubbleGtk() { 62 label_.Destroy(); 63 container_.Destroy(); 64 } 65 66 void StatusBubbleGtk::SetStatus(const base::string16& status_text_wide) { 67 std::string status_text = UTF16ToUTF8(status_text_wide); 68 if (status_text_ == status_text) 69 return; 70 71 status_text_ = status_text; 72 if (!status_text_.empty()) 73 SetStatusTextTo(status_text_); 74 else if (!url_text_.empty()) 75 SetStatusTextTo(url_text_); 76 else 77 SetStatusTextTo(std::string()); 78 } 79 80 void StatusBubbleGtk::SetURL(const GURL& url, const std::string& languages) { 81 url_ = url; 82 languages_ = languages; 83 84 // If we want to clear a displayed URL but there is a status still to 85 // display, display that status instead. 86 if (url.is_empty() && !status_text_.empty()) { 87 url_text_ = std::string(); 88 SetStatusTextTo(status_text_); 89 return; 90 } 91 92 SetStatusTextToURL(); 93 } 94 95 void StatusBubbleGtk::SetStatusTextToURL() { 96 GtkWidget* parent = gtk_widget_get_parent(container_.get()); 97 98 // It appears that parent can be NULL (probably only during shutdown). 99 if (!parent || !gtk_widget_get_realized(parent)) 100 return; 101 102 GtkAllocation allocation; 103 gtk_widget_get_allocation(parent, &allocation); 104 int desired_width = allocation.width; 105 if (!expanded()) { 106 expand_timer_.Stop(); 107 expand_timer_.Start(FROM_HERE, 108 base::TimeDelta::FromMilliseconds(kExpandHoverDelay), 109 this, &StatusBubbleGtk::ExpandURL); 110 // When not expanded, we limit the size to one third the browser's 111 // width. 112 desired_width /= 3; 113 } 114 115 // TODO(tc): We don't actually use gfx::Font as the font in the status 116 // bubble. We should extend gfx::ElideUrl to take some sort of pango font. 117 url_text_ = UTF16ToUTF8( 118 gfx::ElideUrl(url_, gfx::FontList(), desired_width, languages_)); 119 SetStatusTextTo(url_text_); 120 } 121 122 void StatusBubbleGtk::Show() { 123 // If we were going to hide, stop. 124 hide_timer_.Stop(); 125 126 gtk_widget_show(container_.get()); 127 GdkWindow* gdk_window = gtk_widget_get_window(container_.get()); 128 if (gdk_window) 129 gdk_window_raise(gdk_window); 130 } 131 132 void StatusBubbleGtk::Hide() { 133 // If we were going to expand the bubble, stop. 134 expand_timer_.Stop(); 135 expand_animation_.reset(); 136 137 gtk_widget_hide(container_.get()); 138 } 139 140 void StatusBubbleGtk::SetStatusTextTo(const std::string& status_utf8) { 141 if (status_utf8.empty()) { 142 hide_timer_.Stop(); 143 hide_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(kHideDelay), 144 this, &StatusBubbleGtk::Hide); 145 } else { 146 gtk_label_set_text(GTK_LABEL(label_.get()), status_utf8.c_str()); 147 GtkRequisition req; 148 gtk_widget_size_request(label_.get(), &req); 149 desired_width_ = req.width; 150 151 UpdateLabelSizeRequest(); 152 153 if (!last_mouse_left_content_) { 154 // Show the padding and label to update our requisition and then 155 // re-process the last mouse event -- if the label was empty before or the 156 // text changed, our size will have changed and we may need to move 157 // ourselves away from the pointer now. 158 gtk_widget_show_all(padding_); 159 MouseMoved(last_mouse_location_, false); 160 } 161 Show(); 162 } 163 } 164 165 void StatusBubbleGtk::MouseMoved( 166 const gfx::Point& location, bool left_content) { 167 if (left_content && ignore_next_left_content_) { 168 ignore_next_left_content_ = false; 169 return; 170 } 171 172 last_mouse_location_ = location; 173 last_mouse_left_content_ = left_content; 174 175 if (!gtk_widget_get_realized(container_.get())) 176 return; 177 178 GtkWidget* parent = gtk_widget_get_parent(container_.get()); 179 if (!parent || !gtk_widget_get_realized(parent)) 180 return; 181 182 int old_y_offset = y_offset_; 183 bool old_flip_horizontally = flip_horizontally_; 184 185 if (left_content) { 186 SetFlipHorizontally(false); 187 y_offset_ = 0; 188 } else { 189 GtkWidget* toplevel = gtk_widget_get_toplevel(container_.get()); 190 if (!toplevel || !gtk_widget_get_realized(toplevel)) 191 return; 192 193 bool ltr = !base::i18n::IsRTL(); 194 195 GtkRequisition requisition; 196 gtk_widget_size_request(container_.get(), &requisition); 197 198 GtkAllocation parent_allocation; 199 gtk_widget_get_allocation(parent, &parent_allocation); 200 201 // Get our base position (that is, not including the current offset) 202 // relative to the origin of the root window. 203 gint toplevel_x = 0, toplevel_y = 0; 204 GdkWindow* gdk_window = gtk_widget_get_window(toplevel); 205 gdk_window_get_position(gdk_window, &toplevel_x, &toplevel_y); 206 gfx::Rect parent_rect = 207 gtk_util::GetWidgetRectRelativeToToplevel(parent); 208 gfx::Rect bubble_rect( 209 toplevel_x + parent_rect.x() + 210 (ltr ? 0 : parent_allocation.width - requisition.width), 211 toplevel_y + parent_rect.y() + 212 parent_allocation.height - requisition.height, 213 requisition.width, 214 requisition.height); 215 216 int left_threshold = 217 bubble_rect.x() - bubble_rect.height() - kMousePadding; 218 int right_threshold = 219 bubble_rect.right() + bubble_rect.height() + kMousePadding; 220 int top_threshold = bubble_rect.y() - kMousePadding; 221 222 if (((ltr && location.x() < right_threshold) || 223 (!ltr && location.x() > left_threshold)) && 224 location.y() > top_threshold) { 225 if (download_shelf_is_visible_) { 226 SetFlipHorizontally(true); 227 y_offset_ = 0; 228 } else { 229 SetFlipHorizontally(false); 230 int distance = std::max(ltr ? 231 location.x() - right_threshold : 232 left_threshold - location.x(), 233 top_threshold - location.y()); 234 y_offset_ = std::min(-1 * distance, requisition.height); 235 } 236 } else { 237 SetFlipHorizontally(false); 238 y_offset_ = 0; 239 } 240 } 241 242 if (y_offset_ != old_y_offset || flip_horizontally_ != old_flip_horizontally) 243 gtk_widget_queue_resize_no_redraw(parent); 244 } 245 246 void StatusBubbleGtk::UpdateDownloadShelfVisibility(bool visible) { 247 download_shelf_is_visible_ = visible; 248 } 249 250 void StatusBubbleGtk::Observe(int type, 251 const content::NotificationSource& source, 252 const content::NotificationDetails& details) { 253 if (type == chrome::NOTIFICATION_BROWSER_THEME_CHANGED) { 254 UserChangedTheme(); 255 } 256 } 257 258 void StatusBubbleGtk::InitWidgets() { 259 bool ltr = !base::i18n::IsRTL(); 260 261 label_.Own(gtk_label_new(NULL)); 262 263 padding_ = gtk_alignment_new(0, 0, 1, 1); 264 gtk_alignment_set_padding(GTK_ALIGNMENT(padding_), 265 kInternalTopBottomPadding, kInternalTopBottomPadding, 266 kInternalLeftRightPadding + (ltr ? 0 : kCornerSize), 267 kInternalLeftRightPadding + (ltr ? kCornerSize : 0)); 268 gtk_container_add(GTK_CONTAINER(padding_), label_.get()); 269 gtk_widget_show_all(padding_); 270 271 container_.Own(gtk_event_box_new()); 272 gtk_widget_set_no_show_all(container_.get(), TRUE); 273 gtk_util::ActAsRoundedWindow( 274 container_.get(), ui::kGdkWhite, kCornerSize, 275 gtk_util::ROUNDED_TOP_RIGHT, 276 gtk_util::BORDER_TOP | gtk_util::BORDER_RIGHT); 277 gtk_widget_set_name(container_.get(), "status-bubble"); 278 gtk_container_add(GTK_CONTAINER(container_.get()), padding_); 279 280 // We need to listen for mouse motion events, since a fast-moving pointer may 281 // enter our window without us getting any motion events on the browser near 282 // enough for us to run away. 283 gtk_widget_add_events(container_.get(), GDK_POINTER_MOTION_MASK | 284 GDK_ENTER_NOTIFY_MASK); 285 g_signal_connect(container_.get(), "motion-notify-event", 286 G_CALLBACK(HandleMotionNotifyThunk), this); 287 g_signal_connect(container_.get(), "enter-notify-event", 288 G_CALLBACK(HandleEnterNotifyThunk), this); 289 290 UserChangedTheme(); 291 } 292 293 void StatusBubbleGtk::UserChangedTheme() { 294 if (theme_service_->UsingNativeTheme()) { 295 gtk_widget_modify_fg(label_.get(), GTK_STATE_NORMAL, NULL); 296 gtk_widget_modify_bg(container_.get(), GTK_STATE_NORMAL, NULL); 297 } else { 298 // TODO(erg): This is the closest to "text that will look good on a 299 // toolbar" that I can find. Maybe in later iterations of the theme system, 300 // there will be a better color to pick. 301 GdkColor bookmark_text = 302 theme_service_->GetGdkColor(ThemeProperties::COLOR_BOOKMARK_TEXT); 303 gtk_widget_modify_fg(label_.get(), GTK_STATE_NORMAL, &bookmark_text); 304 305 GdkColor toolbar_color = 306 theme_service_->GetGdkColor(ThemeProperties::COLOR_TOOLBAR); 307 gtk_widget_modify_bg(container_.get(), GTK_STATE_NORMAL, &toolbar_color); 308 } 309 310 gtk_util::SetRoundedWindowBorderColor(container_.get(), 311 theme_service_->GetBorderColor()); 312 } 313 314 void StatusBubbleGtk::SetFlipHorizontally(bool flip_horizontally) { 315 if (flip_horizontally == flip_horizontally_) 316 return; 317 318 flip_horizontally_ = flip_horizontally; 319 320 bool ltr = !base::i18n::IsRTL(); 321 bool on_left = (ltr && !flip_horizontally) || (!ltr && flip_horizontally); 322 323 gtk_alignment_set_padding(GTK_ALIGNMENT(padding_), 324 kInternalTopBottomPadding, kInternalTopBottomPadding, 325 kInternalLeftRightPadding + (on_left ? 0 : kCornerSize), 326 kInternalLeftRightPadding + (on_left ? kCornerSize : 0)); 327 // The rounded window code flips these arguments if we're RTL. 328 gtk_util::SetRoundedWindowEdgesAndBorders( 329 container_.get(), 330 kCornerSize, 331 flip_horizontally ? 332 gtk_util::ROUNDED_TOP_LEFT : 333 gtk_util::ROUNDED_TOP_RIGHT, 334 gtk_util::BORDER_TOP | 335 (flip_horizontally ? gtk_util::BORDER_LEFT : gtk_util::BORDER_RIGHT)); 336 gtk_widget_queue_draw(container_.get()); 337 } 338 339 void StatusBubbleGtk::ExpandURL() { 340 GtkAllocation allocation; 341 gtk_widget_get_allocation(label_.get(), &allocation); 342 start_width_ = allocation.width; 343 expand_animation_.reset(new gfx::SlideAnimation(this)); 344 expand_animation_->SetTweenType(gfx::Tween::LINEAR); 345 expand_animation_->Show(); 346 347 SetStatusTextToURL(); 348 } 349 350 void StatusBubbleGtk::UpdateLabelSizeRequest() { 351 if (!expanded() || !expand_animation_->is_animating()) { 352 gtk_widget_set_size_request(label_.get(), -1, -1); 353 return; 354 } 355 356 int new_width = start_width_ + 357 (desired_width_ - start_width_) * expand_animation_->GetCurrentValue(); 358 gtk_widget_set_size_request(label_.get(), new_width, -1); 359 } 360 361 // See http://crbug.com/68897 for why we have to handle this event. 362 gboolean StatusBubbleGtk::HandleEnterNotify(GtkWidget* sender, 363 GdkEventCrossing* event) { 364 ignore_next_left_content_ = true; 365 MouseMoved(gfx::Point(event->x_root, event->y_root), false); 366 return FALSE; 367 } 368 369 gboolean StatusBubbleGtk::HandleMotionNotify(GtkWidget* sender, 370 GdkEventMotion* event) { 371 MouseMoved(gfx::Point(event->x_root, event->y_root), false); 372 return FALSE; 373 } 374 375 void StatusBubbleGtk::AnimationEnded(const gfx::Animation* animation) { 376 UpdateLabelSizeRequest(); 377 } 378 379 void StatusBubbleGtk::AnimationProgressed(const gfx::Animation* animation) { 380 UpdateLabelSizeRequest(); 381 } 382