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