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