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
21namespace {
22
23// The height of the arrow, and the width will be about twice the height.
24const int kArrowSize = 8;
25
26// Number of pixels to the middle of the arrow from the close edge of the
27// window.
28const int kArrowX = 18;
29
30// Number of pixels between the tip of the arrow and the region we're
31// pointing to.
32const int kArrowToContentPadding = -4;
33
34// We draw flat diagonal corners, each corner is an NxN square.
35const int kCornerSize = 3;
36
37// Margins around the content.
38const int kTopMargin = kArrowSize + kCornerSize - 1;
39const int kBottomMargin = kCornerSize - 1;
40const int kLeftMargin = kCornerSize - 1;
41const int kRightMargin = kCornerSize - 1;
42
43const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xff, 0xff, 0xff);
44const GdkColor kFrameColor = GDK_COLOR_RGB(0x63, 0x63, 0x63);
45
46}  // namespace
47
48// static
49InfoBubbleGtk* 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
63InfoBubbleGtk::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
79InfoBubbleGtk::~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
90void 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
195std::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
252InfoBubbleGtk::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
269bool 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
296void 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
312void 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
340void 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
346void 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
358void InfoBubbleGtk::HandlePointerAndKeyboardUngrabbedByContent() {
359  if (grab_input_)
360    GrabPointerAndKeyboard();
361}
362
363void 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
371void 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
397gboolean 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
454gboolean 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.
472void 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
481gboolean 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
507gboolean 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
515void InfoBubbleGtk::OnHide(GtkWidget* widget) {
516  gtk_widget_destroy(widget);
517}
518
519gboolean InfoBubbleGtk::OnToplevelConfigure(GtkWidget* widget,
520                                            GdkEventConfigure* event) {
521  if (!UpdateArrowLocation(false))
522    MoveWindow();
523  StackWindow();
524  return FALSE;
525}
526
527gboolean InfoBubbleGtk::OnToplevelUnmap(GtkWidget* widget, GdkEvent* event) {
528  Close();
529  return FALSE;
530}
531
532void InfoBubbleGtk::OnAnchorAllocate(GtkWidget* widget,
533                                     GtkAllocation* allocation) {
534  if (!UpdateArrowLocation(false))
535    MoveWindow();
536}
537