• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/info_bubble_gtk.h"
6 
7 #include <gdk/gdkkeysyms.h>
8 #include <vector>
9 
10 #include "base/basictypes.h"
11 #include "base/logging.h"
12 #include "chrome/browser/ui/gtk/gtk_theme_service.h"
13 #include "chrome/browser/ui/gtk/gtk_util.h"
14 #include "chrome/browser/ui/gtk/info_bubble_accelerators_gtk.h"
15 #include "content/common/notification_service.h"
16 #include "ui/base/gtk/gtk_windowing.h"
17 #include "ui/gfx/gtk_util.h"
18 #include "ui/gfx/path.h"
19 #include "ui/gfx/rect.h"
20 
21 namespace {
22 
23 // The height of the arrow, and the width will be about twice the height.
24 const int kArrowSize = 8;
25 
26 // Number of pixels to the middle of the arrow from the close edge of the
27 // window.
28 const int kArrowX = 18;
29 
30 // Number of pixels between the tip of the arrow and the region we're
31 // pointing to.
32 const int kArrowToContentPadding = -4;
33 
34 // We draw flat diagonal corners, each corner is an NxN square.
35 const int kCornerSize = 3;
36 
37 // Margins around the content.
38 const int kTopMargin = kArrowSize + kCornerSize - 1;
39 const int kBottomMargin = kCornerSize - 1;
40 const int kLeftMargin = kCornerSize - 1;
41 const int kRightMargin = kCornerSize - 1;
42 
43 const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xff, 0xff, 0xff);
44 const GdkColor kFrameColor = GDK_COLOR_RGB(0x63, 0x63, 0x63);
45 
46 }  // namespace
47 
48 // static
Show(GtkWidget * anchor_widget,const gfx::Rect * rect,GtkWidget * content,ArrowLocationGtk arrow_location,bool match_system_theme,bool grab_input,GtkThemeService * provider,InfoBubbleGtkDelegate * delegate)49 InfoBubbleGtk* InfoBubbleGtk::Show(GtkWidget* anchor_widget,
50                                    const gfx::Rect* rect,
51                                    GtkWidget* content,
52                                    ArrowLocationGtk arrow_location,
53                                    bool match_system_theme,
54                                    bool grab_input,
55                                    GtkThemeService* provider,
56                                    InfoBubbleGtkDelegate* delegate) {
57   InfoBubbleGtk* bubble = new InfoBubbleGtk(provider, match_system_theme);
58   bubble->Init(anchor_widget, rect, content, arrow_location, grab_input);
59   bubble->set_delegate(delegate);
60   return bubble;
61 }
62 
InfoBubbleGtk(GtkThemeService * provider,bool match_system_theme)63 InfoBubbleGtk::InfoBubbleGtk(GtkThemeService* provider,
64                              bool match_system_theme)
65     : delegate_(NULL),
66       window_(NULL),
67       theme_service_(provider),
68       accel_group_(gtk_accel_group_new()),
69       toplevel_window_(NULL),
70       anchor_widget_(NULL),
71       mask_region_(NULL),
72       preferred_arrow_location_(ARROW_LOCATION_TOP_LEFT),
73       current_arrow_location_(ARROW_LOCATION_TOP_LEFT),
74       match_system_theme_(match_system_theme),
75       grab_input_(true),
76       closed_by_escape_(false) {
77 }
78 
~InfoBubbleGtk()79 InfoBubbleGtk::~InfoBubbleGtk() {
80   // Notify the delegate that we're about to close.  This gives the chance
81   // to save state / etc from the hosted widget before it's destroyed.
82   if (delegate_)
83     delegate_->InfoBubbleClosing(this, closed_by_escape_);
84 
85   g_object_unref(accel_group_);
86   if (mask_region_)
87     gdk_region_destroy(mask_region_);
88 }
89 
Init(GtkWidget * anchor_widget,const gfx::Rect * rect,GtkWidget * content,ArrowLocationGtk arrow_location,bool grab_input)90 void InfoBubbleGtk::Init(GtkWidget* anchor_widget,
91                          const gfx::Rect* rect,
92                          GtkWidget* content,
93                          ArrowLocationGtk arrow_location,
94                          bool grab_input) {
95   // If there is a current grab widget (menu, other info bubble, etc.), hide it.
96   GtkWidget* current_grab_widget = gtk_grab_get_current();
97   if (current_grab_widget)
98     gtk_widget_hide(current_grab_widget);
99 
100   DCHECK(!window_);
101   anchor_widget_ = anchor_widget;
102   toplevel_window_ = GTK_WINDOW(gtk_widget_get_toplevel(anchor_widget_));
103   DCHECK(GTK_WIDGET_TOPLEVEL(toplevel_window_));
104   rect_ = rect ? *rect : gtk_util::WidgetBounds(anchor_widget);
105   preferred_arrow_location_ = arrow_location;
106 
107   grab_input_ = grab_input;
108   // Using a TOPLEVEL window may cause placement issues with certain WMs but it
109   // is necessary to be able to focus the window.
110   window_ = gtk_window_new(grab_input ? GTK_WINDOW_POPUP : GTK_WINDOW_TOPLEVEL);
111 
112   gtk_widget_set_app_paintable(window_, TRUE);
113   // Resizing is handled by the program, not user.
114   gtk_window_set_resizable(GTK_WINDOW(window_), FALSE);
115 
116   // Attach all of the accelerators to the bubble.
117   InfoBubbleAcceleratorGtkList acceleratorList =
118       InfoBubbleAcceleratorsGtk::GetList();
119   for (InfoBubbleAcceleratorGtkList::const_iterator iter =
120            acceleratorList.begin();
121        iter != acceleratorList.end();
122        ++iter) {
123     gtk_accel_group_connect(accel_group_,
124                             iter->keyval,
125                             iter->modifier_type,
126                             GtkAccelFlags(0),
127                             g_cclosure_new(G_CALLBACK(&OnGtkAcceleratorThunk),
128                                            this,
129                                            NULL));
130   }
131 
132   gtk_window_add_accel_group(GTK_WINDOW(window_), accel_group_);
133 
134   GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
135   gtk_alignment_set_padding(GTK_ALIGNMENT(alignment),
136                             kTopMargin, kBottomMargin,
137                             kLeftMargin, kRightMargin);
138 
139   gtk_container_add(GTK_CONTAINER(alignment), content);
140   gtk_container_add(GTK_CONTAINER(window_), alignment);
141 
142   // GtkWidget only exposes the bitmap mask interface.  Use GDK to more
143   // efficently mask a GdkRegion.  Make sure the window is realized during
144   // OnSizeAllocate, so the mask can be applied to the GdkWindow.
145   gtk_widget_realize(window_);
146 
147   UpdateArrowLocation(true);  // Force move and reshape.
148   StackWindow();
149 
150   gtk_widget_add_events(window_, GDK_BUTTON_PRESS_MASK);
151 
152   signals_.Connect(window_, "expose-event", G_CALLBACK(OnExposeThunk), this);
153   signals_.Connect(window_, "size-allocate", G_CALLBACK(OnSizeAllocateThunk),
154                    this);
155   signals_.Connect(window_, "button-press-event",
156                    G_CALLBACK(OnButtonPressThunk), this);
157   signals_.Connect(window_, "destroy", G_CALLBACK(OnDestroyThunk), this);
158   signals_.Connect(window_, "hide", G_CALLBACK(OnHideThunk), this);
159 
160   // If the toplevel window is being used as the anchor, then the signals below
161   // are enough to keep us positioned correctly.
162   if (anchor_widget_ != GTK_WIDGET(toplevel_window_)) {
163     signals_.Connect(anchor_widget_, "size-allocate",
164                      G_CALLBACK(OnAnchorAllocateThunk), this);
165     signals_.Connect(anchor_widget_, "destroy",
166                      G_CALLBACK(gtk_widget_destroyed), &anchor_widget_);
167   }
168 
169   signals_.Connect(toplevel_window_, "configure-event",
170                    G_CALLBACK(OnToplevelConfigureThunk), this);
171   signals_.Connect(toplevel_window_, "unmap-event",
172                    G_CALLBACK(OnToplevelUnmapThunk), this);
173   // Set |toplevel_window_| to NULL if it gets destroyed.
174   signals_.Connect(toplevel_window_, "destroy",
175                    G_CALLBACK(gtk_widget_destroyed), &toplevel_window_);
176 
177   gtk_widget_show_all(window_);
178 
179   if (grab_input_) {
180     gtk_grab_add(window_);
181     GrabPointerAndKeyboard();
182   }
183 
184   registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED,
185                  NotificationService::AllSources());
186   theme_service_->InitThemesFor(this);
187 }
188 
189 // NOTE: This seems a bit overcomplicated, but it requires a bunch of careful
190 // fudging to get the pixels rasterized exactly where we want them, the arrow to
191 // have a 1 pixel point, etc.
192 // TODO(deanm): Windows draws with Skia and uses some PNG images for the
193 // corners.  This is a lot more work, but they get anti-aliasing.
194 // static
MakeFramePolygonPoints(ArrowLocationGtk arrow_location,int width,int height,FrameType type)195 std::vector<GdkPoint> InfoBubbleGtk::MakeFramePolygonPoints(
196     ArrowLocationGtk arrow_location,
197     int width,
198     int height,
199     FrameType type) {
200   using gtk_util::MakeBidiGdkPoint;
201   std::vector<GdkPoint> points;
202 
203   bool on_left = (arrow_location == ARROW_LOCATION_TOP_LEFT);
204 
205   // If we're stroking the frame, we need to offset some of our points by 1
206   // pixel.  We do this when we draw horizontal lines that are on the bottom or
207   // when we draw vertical lines that are closer to the end (where "end" is the
208   // right side for ARROW_LOCATION_TOP_LEFT).
209   int y_off = (type == FRAME_MASK) ? 0 : -1;
210   // We use this one for arrows located on the left.
211   int x_off_l = on_left ? y_off : 0;
212   // We use this one for RTL.
213   int x_off_r = !on_left ? -y_off : 0;
214 
215   // Top left corner.
216   points.push_back(MakeBidiGdkPoint(
217       x_off_r, kArrowSize + kCornerSize - 1, width, on_left));
218   points.push_back(MakeBidiGdkPoint(
219       kCornerSize + x_off_r - 1, kArrowSize, width, on_left));
220 
221   // The arrow.
222   points.push_back(MakeBidiGdkPoint(
223       kArrowX - kArrowSize + x_off_r, kArrowSize, width, on_left));
224   points.push_back(MakeBidiGdkPoint(
225       kArrowX + x_off_r, 0, width, on_left));
226   points.push_back(MakeBidiGdkPoint(
227       kArrowX + 1 + x_off_l, 0, width, on_left));
228   points.push_back(MakeBidiGdkPoint(
229       kArrowX + kArrowSize + 1 + x_off_l, kArrowSize, width, on_left));
230 
231   // Top right corner.
232   points.push_back(MakeBidiGdkPoint(
233       width - kCornerSize + 1 + x_off_l, kArrowSize, width, on_left));
234   points.push_back(MakeBidiGdkPoint(
235       width + x_off_l, kArrowSize + kCornerSize - 1, width, on_left));
236 
237   // Bottom right corner.
238   points.push_back(MakeBidiGdkPoint(
239       width + x_off_l, height - kCornerSize, width, on_left));
240   points.push_back(MakeBidiGdkPoint(
241       width - kCornerSize + x_off_r, height + y_off, width, on_left));
242 
243   // Bottom left corner.
244   points.push_back(MakeBidiGdkPoint(
245       kCornerSize + x_off_l, height + y_off, width, on_left));
246   points.push_back(MakeBidiGdkPoint(
247       x_off_r, height - kCornerSize, width, on_left));
248 
249   return points;
250 }
251 
GetArrowLocation(ArrowLocationGtk preferred_location,int arrow_x,int width)252 InfoBubbleGtk::ArrowLocationGtk InfoBubbleGtk::GetArrowLocation(
253     ArrowLocationGtk preferred_location, int arrow_x, int width) {
254   bool wants_left = (preferred_location == ARROW_LOCATION_TOP_LEFT);
255   int screen_width = gdk_screen_get_width(gdk_screen_get_default());
256 
257   bool left_is_onscreen = (arrow_x - kArrowX + width < screen_width);
258   bool right_is_onscreen = (arrow_x + kArrowX - width >= 0);
259 
260   // Use the requested location if it fits onscreen, use whatever fits
261   // otherwise, and use the requested location if neither fits.
262   if (left_is_onscreen && (wants_left || !right_is_onscreen))
263     return ARROW_LOCATION_TOP_LEFT;
264   if (right_is_onscreen && (!wants_left || !left_is_onscreen))
265     return ARROW_LOCATION_TOP_RIGHT;
266   return (wants_left ? ARROW_LOCATION_TOP_LEFT : ARROW_LOCATION_TOP_RIGHT);
267 }
268 
UpdateArrowLocation(bool force_move_and_reshape)269 bool InfoBubbleGtk::UpdateArrowLocation(bool force_move_and_reshape) {
270   if (!toplevel_window_ || !anchor_widget_)
271     return false;
272 
273   gint toplevel_x = 0, toplevel_y = 0;
274   gdk_window_get_position(
275       GTK_WIDGET(toplevel_window_)->window, &toplevel_x, &toplevel_y);
276   int offset_x, offset_y;
277   gtk_widget_translate_coordinates(anchor_widget_, GTK_WIDGET(toplevel_window_),
278                                    rect_.x(), rect_.y(), &offset_x, &offset_y);
279 
280   ArrowLocationGtk old_location = current_arrow_location_;
281   current_arrow_location_ = GetArrowLocation(
282       preferred_arrow_location_,
283       toplevel_x + offset_x + (rect_.width() / 2),  // arrow_x
284       window_->allocation.width);
285 
286   if (force_move_and_reshape || current_arrow_location_ != old_location) {
287     UpdateWindowShape();
288     MoveWindow();
289     // We need to redraw the entire window to repaint its border.
290     gtk_widget_queue_draw(window_);
291     return true;
292   }
293   return false;
294 }
295 
UpdateWindowShape()296 void InfoBubbleGtk::UpdateWindowShape() {
297   if (mask_region_) {
298     gdk_region_destroy(mask_region_);
299     mask_region_ = NULL;
300   }
301   std::vector<GdkPoint> points = MakeFramePolygonPoints(
302       current_arrow_location_,
303       window_->allocation.width, window_->allocation.height,
304       FRAME_MASK);
305   mask_region_ = gdk_region_polygon(&points[0],
306                                     points.size(),
307                                     GDK_EVEN_ODD_RULE);
308   gdk_window_shape_combine_region(window_->window, NULL, 0, 0);
309   gdk_window_shape_combine_region(window_->window, mask_region_, 0, 0);
310 }
311 
MoveWindow()312 void InfoBubbleGtk::MoveWindow() {
313   if (!toplevel_window_ || !anchor_widget_)
314     return;
315 
316   gint toplevel_x = 0, toplevel_y = 0;
317   gdk_window_get_position(
318       GTK_WIDGET(toplevel_window_)->window, &toplevel_x, &toplevel_y);
319 
320   int offset_x, offset_y;
321   gtk_widget_translate_coordinates(anchor_widget_, GTK_WIDGET(toplevel_window_),
322                                    rect_.x(), rect_.y(), &offset_x, &offset_y);
323 
324   gint screen_x = 0;
325   if (current_arrow_location_ == ARROW_LOCATION_TOP_LEFT) {
326     screen_x = toplevel_x + offset_x + (rect_.width() / 2) - kArrowX;
327   } else if (current_arrow_location_ == ARROW_LOCATION_TOP_RIGHT) {
328     screen_x = toplevel_x + offset_x + (rect_.width() / 2) -
329                window_->allocation.width + kArrowX;
330   } else {
331     NOTREACHED();
332   }
333 
334   gint screen_y = toplevel_y + offset_y + rect_.height() +
335                   kArrowToContentPadding;
336 
337   gtk_window_move(GTK_WINDOW(window_), screen_x, screen_y);
338 }
339 
StackWindow()340 void InfoBubbleGtk::StackWindow() {
341   // Stack our window directly above the toplevel window.
342   if (toplevel_window_)
343     ui::StackPopupWindow(window_, GTK_WIDGET(toplevel_window_));
344 }
345 
Observe(NotificationType type,const NotificationSource & source,const NotificationDetails & details)346 void InfoBubbleGtk::Observe(NotificationType type,
347                             const NotificationSource& source,
348                             const NotificationDetails& details) {
349   DCHECK_EQ(type.value, NotificationType::BROWSER_THEME_CHANGED);
350   if (theme_service_->UseGtkTheme() && match_system_theme_) {
351     gtk_widget_modify_bg(window_, GTK_STATE_NORMAL, NULL);
352   } else {
353     // Set the background color, so we don't need to paint it manually.
354     gtk_widget_modify_bg(window_, GTK_STATE_NORMAL, &kBackgroundColor);
355   }
356 }
357 
HandlePointerAndKeyboardUngrabbedByContent()358 void InfoBubbleGtk::HandlePointerAndKeyboardUngrabbedByContent() {
359   if (grab_input_)
360     GrabPointerAndKeyboard();
361 }
362 
Close()363 void InfoBubbleGtk::Close() {
364   // We don't need to ungrab the pointer or keyboard here; the X server will
365   // automatically do that when we destroy our window.
366   DCHECK(window_);
367   gtk_widget_destroy(window_);
368   // |this| has been deleted, see OnDestroy.
369 }
370 
GrabPointerAndKeyboard()371 void InfoBubbleGtk::GrabPointerAndKeyboard() {
372   // Install X pointer and keyboard grabs to make sure that we have the focus
373   // and get all mouse and keyboard events until we're closed.
374   GdkGrabStatus pointer_grab_status =
375       gdk_pointer_grab(window_->window,
376                        TRUE,                   // owner_events
377                        GDK_BUTTON_PRESS_MASK,  // event_mask
378                        NULL,                   // confine_to
379                        NULL,                   // cursor
380                        GDK_CURRENT_TIME);
381   if (pointer_grab_status != GDK_GRAB_SUCCESS) {
382     // This will fail if someone else already has the pointer grabbed, but
383     // there's not really anything we can do about that.
384     DLOG(ERROR) << "Unable to grab pointer (status="
385                 << pointer_grab_status << ")";
386   }
387   GdkGrabStatus keyboard_grab_status =
388       gdk_keyboard_grab(window_->window,
389                         FALSE,  // owner_events
390                         GDK_CURRENT_TIME);
391   if (keyboard_grab_status != GDK_GRAB_SUCCESS) {
392     DLOG(ERROR) << "Unable to grab keyboard (status="
393                 << keyboard_grab_status << ")";
394   }
395 }
396 
OnGtkAccelerator(GtkAccelGroup * group,GObject * acceleratable,guint keyval,GdkModifierType modifier)397 gboolean InfoBubbleGtk::OnGtkAccelerator(GtkAccelGroup* group,
398                                          GObject* acceleratable,
399                                          guint keyval,
400                                          GdkModifierType modifier) {
401   GdkEventKey msg;
402   GdkKeymapKey* keys;
403   gint n_keys;
404 
405   switch (keyval) {
406     case GDK_Escape:
407       // Close on Esc and trap the accelerator
408       closed_by_escape_ = true;
409       Close();
410       return TRUE;
411     case GDK_w:
412       // Close on C-w and forward the accelerator
413       if (modifier & GDK_CONTROL_MASK) {
414         Close();
415       }
416       break;
417     default:
418       return FALSE;
419   }
420 
421   gdk_keymap_get_entries_for_keyval(NULL,
422                                     keyval,
423                                     &keys,
424                                     &n_keys);
425   if (n_keys) {
426     // Forward the accelerator to root window the bubble is anchored
427     // to for further processing
428     msg.type = GDK_KEY_PRESS;
429     msg.window = GTK_WIDGET(toplevel_window_)->window;
430     msg.send_event = TRUE;
431     msg.time = GDK_CURRENT_TIME;
432     msg.state = modifier | GDK_MOD2_MASK;
433     msg.keyval = keyval;
434     // length and string are deprecated and thus zeroed out
435     msg.length = 0;
436     msg.string = NULL;
437     msg.hardware_keycode = keys[0].keycode;
438     msg.group = keys[0].group;
439     msg.is_modifier = 0;
440 
441     g_free(keys);
442 
443     gtk_main_do_event(reinterpret_cast<GdkEvent*>(&msg));
444   } else {
445     // This means that there isn't a h/w code for the keyval in the
446     // current keymap, which is weird but possible if the keymap just
447     // changed. This isn't a critical error, but might be indicative
448     // of something off if it happens regularly.
449     DLOG(WARNING) << "Found no keys for value " << keyval;
450   }
451   return TRUE;
452 }
453 
OnExpose(GtkWidget * widget,GdkEventExpose * expose)454 gboolean InfoBubbleGtk::OnExpose(GtkWidget* widget, GdkEventExpose* expose) {
455   GdkDrawable* drawable = GDK_DRAWABLE(window_->window);
456   GdkGC* gc = gdk_gc_new(drawable);
457   gdk_gc_set_rgb_fg_color(gc, &kFrameColor);
458 
459   // Stroke the frame border.
460   std::vector<GdkPoint> points = MakeFramePolygonPoints(
461       current_arrow_location_,
462       window_->allocation.width, window_->allocation.height,
463       FRAME_STROKE);
464   gdk_draw_polygon(drawable, gc, FALSE, &points[0], points.size());
465 
466   g_object_unref(gc);
467   return FALSE;  // Propagate so our children paint, etc.
468 }
469 
470 // When our size is initially allocated or changed, we need to recompute
471 // and apply our shape mask region.
OnSizeAllocate(GtkWidget * widget,GtkAllocation * allocation)472 void InfoBubbleGtk::OnSizeAllocate(GtkWidget* widget,
473                                    GtkAllocation* allocation) {
474   if (!UpdateArrowLocation(false)) {
475     UpdateWindowShape();
476     if (current_arrow_location_ == ARROW_LOCATION_TOP_RIGHT)
477       MoveWindow();
478   }
479 }
480 
OnButtonPress(GtkWidget * widget,GdkEventButton * event)481 gboolean InfoBubbleGtk::OnButtonPress(GtkWidget* widget,
482                                       GdkEventButton* event) {
483   // If we got a click in our own window, that's okay (we need to additionally
484   // check that it falls within our bounds, since we've grabbed the pointer and
485   // some events that actually occurred in other windows will be reported with
486   // respect to our window).
487   if (event->window == window_->window &&
488       (mask_region_ && gdk_region_point_in(mask_region_, event->x, event->y))) {
489     return FALSE;  // Propagate.
490   }
491 
492   // Our content widget got a click.
493   if (event->window != window_->window &&
494       gdk_window_get_toplevel(event->window) == window_->window) {
495     return FALSE;
496   }
497 
498   if (grab_input_) {
499     // Otherwise we had a click outside of our window, close ourself.
500     Close();
501     return TRUE;
502   }
503 
504   return FALSE;
505 }
506 
OnDestroy(GtkWidget * widget)507 gboolean InfoBubbleGtk::OnDestroy(GtkWidget* widget) {
508   // We are self deleting, we have a destroy signal setup to catch when we
509   // destroy the widget manually, or the window was closed via X.  This will
510   // delete the InfoBubbleGtk object.
511   delete this;
512   return FALSE;  // Propagate.
513 }
514 
OnHide(GtkWidget * widget)515 void InfoBubbleGtk::OnHide(GtkWidget* widget) {
516   gtk_widget_destroy(widget);
517 }
518 
OnToplevelConfigure(GtkWidget * widget,GdkEventConfigure * event)519 gboolean InfoBubbleGtk::OnToplevelConfigure(GtkWidget* widget,
520                                             GdkEventConfigure* event) {
521   if (!UpdateArrowLocation(false))
522     MoveWindow();
523   StackWindow();
524   return FALSE;
525 }
526 
OnToplevelUnmap(GtkWidget * widget,GdkEvent * event)527 gboolean InfoBubbleGtk::OnToplevelUnmap(GtkWidget* widget, GdkEvent* event) {
528   Close();
529   return FALSE;
530 }
531 
OnAnchorAllocate(GtkWidget * widget,GtkAllocation * allocation)532 void InfoBubbleGtk::OnAnchorAllocate(GtkWidget* widget,
533                                      GtkAllocation* allocation) {
534   if (!UpdateArrowLocation(false))
535     MoveWindow();
536 }
537