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.
5 #include "chrome/browser/ui/gtk/status_bubble_gtk.h"
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"
28 // Inner padding between the border and the text label.
29 const int kInternalTopBottomPadding
= 1;
30 const int kInternalLeftRightPadding
= 2;
32 // The radius of the edges of our bubble.
33 const int kCornerSize
= 3;
35 // Milliseconds before we hide the status bubble widget when you mouseout.
36 const int kHideDelay
= 250;
38 // How close the mouse can get to the infobubble before it starts sliding
40 const int kMousePadding
= 20;
44 StatusBubbleGtk::StatusBubbleGtk(Profile
* profile
)
45 : theme_service_(GtkThemeService::GetFrom(profile
)),
49 flip_horizontally_(false),
51 download_shelf_is_visible_(false),
52 last_mouse_left_content_(false),
53 ignore_next_left_content_(false) {
56 theme_service_
->InitThemesFor(this);
57 registrar_
.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED
,
58 content::Source
<ThemeService
>(theme_service_
));
61 StatusBubbleGtk::~StatusBubbleGtk() {
66 void StatusBubbleGtk::SetStatus(const base::string16
& status_text_wide
) {
67 std::string status_text
= base::UTF16ToUTF8(status_text_wide
);
68 if (status_text_
== status_text
)
71 status_text_
= status_text
;
72 if (!status_text_
.empty())
73 SetStatusTextTo(status_text_
);
74 else if (!url_text_
.empty())
75 SetStatusTextTo(url_text_
);
77 SetStatusTextTo(std::string());
80 void StatusBubbleGtk::SetURL(const GURL
& url
, const std::string
& languages
) {
82 languages_
= languages
;
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_
);
95 void StatusBubbleGtk::SetStatusTextToURL() {
96 GtkWidget
* parent
= gtk_widget_get_parent(container_
.get());
98 // It appears that parent can be NULL (probably only during shutdown).
99 if (!parent
|| !gtk_widget_get_realized(parent
))
102 GtkAllocation allocation
;
103 gtk_widget_get_allocation(parent
, &allocation
);
104 int desired_width
= allocation
.width
;
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
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_
= base::UTF16ToUTF8(
118 gfx::ElideUrl(url_
, gfx::FontList(), desired_width
, languages_
));
119 SetStatusTextTo(url_text_
);
122 void StatusBubbleGtk::Show() {
123 // If we were going to hide, stop.
126 gtk_widget_show(container_
.get());
127 GdkWindow
* gdk_window
= gtk_widget_get_window(container_
.get());
129 gdk_window_raise(gdk_window
);
132 void StatusBubbleGtk::Hide() {
133 // If we were going to expand the bubble, stop.
134 expand_timer_
.Stop();
135 expand_animation_
.reset();
137 gtk_widget_hide(container_
.get());
140 void StatusBubbleGtk::SetStatusTextTo(const std::string
& status_utf8
) {
141 if (status_utf8
.empty()) {
143 hide_timer_
.Start(FROM_HERE
, base::TimeDelta::FromMilliseconds(kHideDelay
),
144 this, &StatusBubbleGtk::Hide
);
146 gtk_label_set_text(GTK_LABEL(label_
.get()), status_utf8
.c_str());
148 gtk_widget_size_request(label_
.get(), &req
);
149 desired_width_
= req
.width
;
151 UpdateLabelSizeRequest();
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);
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;
172 last_mouse_location_
= location
;
173 last_mouse_left_content_
= left_content
;
175 if (!gtk_widget_get_realized(container_
.get()))
178 GtkWidget
* parent
= gtk_widget_get_parent(container_
.get());
179 if (!parent
|| !gtk_widget_get_realized(parent
))
182 int old_y_offset
= y_offset_
;
183 bool old_flip_horizontally
= flip_horizontally_
;
186 SetFlipHorizontally(false);
189 GtkWidget
* toplevel
= gtk_widget_get_toplevel(container_
.get());
190 if (!toplevel
|| !gtk_widget_get_realized(toplevel
))
193 bool ltr
= !base::i18n::IsRTL();
195 GtkRequisition requisition
;
196 gtk_widget_size_request(container_
.get(), &requisition
);
198 GtkAllocation parent_allocation
;
199 gtk_widget_get_allocation(parent
, &parent_allocation
);
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
,
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
;
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);
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
);
237 SetFlipHorizontally(false);
242 if (y_offset_
!= old_y_offset
|| flip_horizontally_
!= old_flip_horizontally
)
243 gtk_widget_queue_resize_no_redraw(parent
);
246 void StatusBubbleGtk::UpdateDownloadShelfVisibility(bool visible
) {
247 download_shelf_is_visible_
= visible
;
250 void StatusBubbleGtk::Observe(int type
,
251 const content::NotificationSource
& source
,
252 const content::NotificationDetails
& details
) {
253 if (type
== chrome::NOTIFICATION_BROWSER_THEME_CHANGED
) {
258 void StatusBubbleGtk::InitWidgets() {
259 bool ltr
= !base::i18n::IsRTL();
261 label_
.Own(gtk_label_new(NULL
));
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_
);
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_
);
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);
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
);
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
);
305 GdkColor toolbar_color
=
306 theme_service_
->GetGdkColor(ThemeProperties::COLOR_TOOLBAR
);
307 gtk_widget_modify_bg(container_
.get(), GTK_STATE_NORMAL
, &toolbar_color
);
310 gtk_util::SetRoundedWindowBorderColor(container_
.get(),
311 theme_service_
->GetBorderColor());
314 void StatusBubbleGtk::SetFlipHorizontally(bool flip_horizontally
) {
315 if (flip_horizontally
== flip_horizontally_
)
318 flip_horizontally_
= flip_horizontally
;
320 bool ltr
= !base::i18n::IsRTL();
321 bool on_left
= (ltr
&& !flip_horizontally
) || (!ltr
&& flip_horizontally
);
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(
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());
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();
347 SetStatusTextToURL();
350 void StatusBubbleGtk::UpdateLabelSizeRequest() {
351 if (!expanded() || !expand_animation_
->is_animating()) {
352 gtk_widget_set_size_request(label_
.get(), -1, -1);
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);
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);
369 gboolean
StatusBubbleGtk::HandleMotionNotify(GtkWidget
* sender
,
370 GdkEventMotion
* event
) {
371 MouseMoved(gfx::Point(event
->x_root
, event
->y_root
), false);
375 void StatusBubbleGtk::AnimationEnded(const gfx::Animation
* animation
) {
376 UpdateLabelSizeRequest();
379 void StatusBubbleGtk::AnimationProgressed(const gfx::Animation
* animation
) {
380 UpdateLabelSizeRequest();