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.
4
5#include "chrome/browser/ui/gtk/bubble/bubble_gtk.h"
6
7#include <gdk/gdkkeysyms.h>
8
9#include "base/i18n/rtl.h"
10#include "chrome/browser/chrome_notification_types.h"
11#include "chrome/browser/ui/gtk/bubble/bubble_accelerators_gtk.h"
12#include "chrome/browser/ui/gtk/gtk_theme_service.h"
13#include "chrome/browser/ui/gtk/gtk_util.h"
14#include "content/public/browser/notification_source.h"
15#include "ui/base/gtk/gtk_compat.h"
16#include "ui/base/gtk/gtk_hig_constants.h"
17#include "ui/base/gtk/gtk_windowing.h"
18#include "ui/gfx/gtk_util.h"
19#include "ui/gfx/path.h"
20#include "ui/gfx/rect.h"
21
22namespace {
23
24// The height of the arrow, and the width will be about twice the height.
25const int kArrowSize = 8;
26
27// Number of pixels to the middle of the arrow from the close edge of the
28// window.
29const int kArrowX = 18;
30
31// Number of pixels between the tip of the arrow and the region we're
32// pointing to.
33const int kArrowToContentPadding = -4;
34
35// We draw flat diagonal corners, each corner is an NxN square.
36const int kCornerSize = 3;
37
38// The amount of padding (in pixels) from the top of |toplevel_window_| to the
39// top of |window_| when fixed positioning is used.
40const int kFixedPositionPaddingEnd = 10;
41const int kFixedPositionPaddingTop = 5;
42
43const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xff, 0xff, 0xff);
44const GdkColor kFrameColor = GDK_COLOR_RGB(0x63, 0x63, 0x63);
45
46// Helper functions that encapsulate arrow locations.
47bool HasArrow(BubbleGtk::FrameStyle frame_style) {
48  return frame_style != BubbleGtk::FLOAT_BELOW_RECT &&
49         frame_style != BubbleGtk::CENTER_OVER_RECT &&
50         frame_style != BubbleGtk::FIXED_TOP_LEFT &&
51         frame_style != BubbleGtk::FIXED_TOP_RIGHT;
52}
53
54bool IsArrowLeft(BubbleGtk::FrameStyle frame_style) {
55  return frame_style == BubbleGtk::ANCHOR_TOP_LEFT ||
56         frame_style == BubbleGtk::ANCHOR_BOTTOM_LEFT;
57}
58
59bool IsArrowMiddle(BubbleGtk::FrameStyle frame_style) {
60  return frame_style == BubbleGtk::ANCHOR_TOP_MIDDLE ||
61         frame_style == BubbleGtk::ANCHOR_BOTTOM_MIDDLE;
62}
63
64bool IsArrowRight(BubbleGtk::FrameStyle frame_style) {
65  return frame_style == BubbleGtk::ANCHOR_TOP_RIGHT ||
66         frame_style == BubbleGtk::ANCHOR_BOTTOM_RIGHT;
67}
68
69bool IsArrowTop(BubbleGtk::FrameStyle frame_style) {
70  return frame_style == BubbleGtk::ANCHOR_TOP_LEFT ||
71         frame_style == BubbleGtk::ANCHOR_TOP_MIDDLE ||
72         frame_style == BubbleGtk::ANCHOR_TOP_RIGHT;
73}
74
75bool IsArrowBottom(BubbleGtk::FrameStyle frame_style) {
76  return frame_style == BubbleGtk::ANCHOR_BOTTOM_LEFT ||
77         frame_style == BubbleGtk::ANCHOR_BOTTOM_MIDDLE ||
78         frame_style == BubbleGtk::ANCHOR_BOTTOM_RIGHT;
79}
80
81bool IsFixed(BubbleGtk::FrameStyle frame_style) {
82  return frame_style == BubbleGtk::FIXED_TOP_LEFT ||
83         frame_style == BubbleGtk::FIXED_TOP_RIGHT;
84}
85
86BubbleGtk::FrameStyle AdjustFrameStyleForLocale(
87    BubbleGtk::FrameStyle frame_style) {
88  // Only RTL requires more work.
89  if (!base::i18n::IsRTL())
90    return frame_style;
91
92  switch (frame_style) {
93    // These don't flip.
94    case BubbleGtk::ANCHOR_TOP_MIDDLE:
95    case BubbleGtk::ANCHOR_BOTTOM_MIDDLE:
96    case BubbleGtk::FLOAT_BELOW_RECT:
97    case BubbleGtk::CENTER_OVER_RECT:
98      return frame_style;
99
100    // These do flip.
101    case BubbleGtk::ANCHOR_TOP_LEFT:
102      return BubbleGtk::ANCHOR_TOP_RIGHT;
103
104    case BubbleGtk::ANCHOR_TOP_RIGHT:
105      return BubbleGtk::ANCHOR_TOP_LEFT;
106
107    case BubbleGtk::ANCHOR_BOTTOM_LEFT:
108      return BubbleGtk::ANCHOR_BOTTOM_RIGHT;
109
110    case BubbleGtk::ANCHOR_BOTTOM_RIGHT:
111      return BubbleGtk::ANCHOR_BOTTOM_LEFT;
112
113    case BubbleGtk::FIXED_TOP_LEFT:
114      return BubbleGtk::FIXED_TOP_RIGHT;
115
116    case BubbleGtk::FIXED_TOP_RIGHT:
117      return BubbleGtk::FIXED_TOP_LEFT;
118  }
119
120  NOTREACHED();
121  return BubbleGtk::ANCHOR_TOP_LEFT;
122}
123
124}  // namespace
125
126// static
127BubbleGtk* BubbleGtk::Show(GtkWidget* anchor_widget,
128                           const gfx::Rect* rect,
129                           GtkWidget* content,
130                           FrameStyle frame_style,
131                           int attribute_flags,
132                           GtkThemeService* provider,
133                           BubbleDelegateGtk* delegate) {
134  BubbleGtk* bubble = new BubbleGtk(provider,
135                                    AdjustFrameStyleForLocale(frame_style),
136                                    attribute_flags);
137  bubble->Init(anchor_widget, rect, content, attribute_flags);
138  bubble->set_delegate(delegate);
139  return bubble;
140}
141
142BubbleGtk::BubbleGtk(GtkThemeService* provider,
143                     FrameStyle frame_style,
144                     int attribute_flags)
145    : delegate_(NULL),
146      window_(NULL),
147      theme_service_(provider),
148      accel_group_(gtk_accel_group_new()),
149      toplevel_window_(NULL),
150      anchor_widget_(NULL),
151      mask_region_(NULL),
152      requested_frame_style_(frame_style),
153      actual_frame_style_(ANCHOR_TOP_LEFT),
154      match_system_theme_(attribute_flags & MATCH_SYSTEM_THEME),
155      grab_input_(attribute_flags & GRAB_INPUT),
156      closed_by_escape_(false) {
157}
158
159BubbleGtk::~BubbleGtk() {
160  // Notify the delegate that we're about to close.  This gives the chance
161  // to save state / etc from the hosted widget before it's destroyed.
162  if (delegate_)
163    delegate_->BubbleClosing(this, closed_by_escape_);
164
165  g_object_unref(accel_group_);
166  if (mask_region_)
167    gdk_region_destroy(mask_region_);
168}
169
170void BubbleGtk::Init(GtkWidget* anchor_widget,
171                     const gfx::Rect* rect,
172                     GtkWidget* content,
173                     int attribute_flags) {
174  // If there is a current grab widget (menu, other bubble, etc.), hide it.
175  GtkWidget* current_grab_widget = gtk_grab_get_current();
176  if (current_grab_widget)
177    gtk_widget_hide(current_grab_widget);
178
179  DCHECK(!window_);
180  anchor_widget_ = anchor_widget;
181  toplevel_window_ = gtk_widget_get_toplevel(anchor_widget_);
182  DCHECK(gtk_widget_is_toplevel(toplevel_window_));
183  rect_ = rect ? *rect : gtk_util::WidgetBounds(anchor_widget);
184
185  // Using a TOPLEVEL window may cause placement issues with certain WMs but it
186  // is necessary to be able to focus the window.
187  window_ = gtk_window_new(attribute_flags & POPUP_WINDOW ?
188                           GTK_WINDOW_POPUP : GTK_WINDOW_TOPLEVEL);
189
190  gtk_widget_set_app_paintable(window_, TRUE);
191  // Resizing is handled by the program, not user.
192  gtk_window_set_resizable(GTK_WINDOW(window_), FALSE);
193
194  if (!(attribute_flags & NO_ACCELERATORS)) {
195    // Attach all of the accelerators to the bubble.
196    for (BubbleAcceleratorsGtk::const_iterator
197             i(BubbleAcceleratorsGtk::begin());
198         i != BubbleAcceleratorsGtk::end();
199         ++i) {
200      gtk_accel_group_connect(accel_group_,
201                              i->keyval,
202                              i->modifier_type,
203                              GtkAccelFlags(0),
204                              g_cclosure_new(G_CALLBACK(&OnGtkAcceleratorThunk),
205                                             this,
206                                             NULL));
207    }
208    gtk_window_add_accel_group(GTK_WINDOW(window_), accel_group_);
209  }
210
211  // |requested_frame_style_| is used instead of |actual_frame_style_| here
212  // because |actual_frame_style_| is only correct after calling
213  // |UpdateFrameStyle()|. Unfortunately, |UpdateFrameStyle()| requires knowing
214  // the size of |window_| (which happens later on).
215  int arrow_padding = HasArrow(requested_frame_style_) ? kArrowSize : 0;
216  GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
217  gtk_alignment_set_padding(GTK_ALIGNMENT(alignment), arrow_padding, 0, 0, 0);
218
219  gtk_container_add(GTK_CONTAINER(alignment), content);
220  gtk_container_add(GTK_CONTAINER(window_), alignment);
221
222  // GtkWidget only exposes the bitmap mask interface.  Use GDK to more
223  // efficently mask a GdkRegion.  Make sure the window is realized during
224  // OnSizeAllocate, so the mask can be applied to the GdkWindow.
225  gtk_widget_realize(window_);
226
227  UpdateFrameStyle(true);  // Force move and reshape.
228  StackWindow();
229
230  gtk_widget_add_events(window_, GDK_BUTTON_PRESS_MASK);
231
232  // Connect during the bubbling phase so the border is always on top.
233  signals_.ConnectAfter(window_, "expose-event",
234                        G_CALLBACK(OnExposeThunk), this);
235  signals_.Connect(window_, "size-allocate", G_CALLBACK(OnSizeAllocateThunk),
236                   this);
237  signals_.Connect(window_, "button-press-event",
238                   G_CALLBACK(OnButtonPressThunk), this);
239  signals_.Connect(window_, "destroy", G_CALLBACK(OnDestroyThunk), this);
240  signals_.Connect(window_, "hide", G_CALLBACK(OnHideThunk), this);
241  if (grab_input_) {
242    signals_.Connect(window_, "grab-broken-event",
243                     G_CALLBACK(OnGrabBrokenThunk), this);
244  }
245
246  // If the toplevel window is being used as the anchor, then the signals below
247  // are enough to keep us positioned correctly.
248  if (anchor_widget_ != toplevel_window_) {
249    signals_.Connect(anchor_widget_, "size-allocate",
250                     G_CALLBACK(OnAnchorAllocateThunk), this);
251    signals_.Connect(anchor_widget_, "destroy",
252                     G_CALLBACK(OnAnchorDestroyThunk), this);
253  }
254
255  signals_.Connect(toplevel_window_, "configure-event",
256                   G_CALLBACK(OnToplevelConfigureThunk), this);
257  signals_.Connect(toplevel_window_, "unmap-event",
258                   G_CALLBACK(OnToplevelUnmapThunk), this);
259  signals_.Connect(window_, "destroy",
260                   G_CALLBACK(OnToplevelDestroyThunk), this);
261
262  gtk_widget_show_all(window_);
263
264  if (grab_input_)
265    gtk_grab_add(window_);
266  GrabPointerAndKeyboard();
267
268  registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
269                 content::Source<ThemeService>(theme_service_));
270  theme_service_->InitThemesFor(this);
271}
272
273// NOTE: This seems a bit overcomplicated, but it requires a bunch of careful
274// fudging to get the pixels rasterized exactly where we want them, the arrow to
275// have a 1 pixel point, etc.
276// TODO(deanm): Windows draws with Skia and uses some PNG images for the
277// corners.  This is a lot more work, but they get anti-aliasing.
278// static
279std::vector<GdkPoint> BubbleGtk::MakeFramePolygonPoints(
280    FrameStyle frame_style,
281    int width,
282    int height,
283    FrameType type) {
284  using gtk_util::MakeBidiGdkPoint;
285  std::vector<GdkPoint> points;
286
287  int top_arrow_size = IsArrowTop(frame_style) ? kArrowSize : 0;
288  int bottom_arrow_size = IsArrowBottom(frame_style) ? kArrowSize : 0;
289  bool on_left = IsArrowLeft(frame_style);
290
291  // If we're stroking the frame, we need to offset some of our points by 1
292  // pixel.  We do this when we draw horizontal lines that are on the bottom or
293  // when we draw vertical lines that are closer to the end (where "end" is the
294  // right side for ANCHOR_TOP_LEFT).
295  int y_off = type == FRAME_MASK ? 0 : -1;
296  // We use this one for arrows located on the left.
297  int x_off_l = on_left ? y_off : 0;
298  // We use this one for RTL.
299  int x_off_r = !on_left ? -y_off : 0;
300
301  // Top left corner.
302  points.push_back(MakeBidiGdkPoint(
303      x_off_r, top_arrow_size + kCornerSize - 1, width, on_left));
304  points.push_back(MakeBidiGdkPoint(
305      kCornerSize + x_off_r - 1, top_arrow_size, width, on_left));
306
307  // The top arrow.
308  if (top_arrow_size) {
309    int arrow_x = frame_style == ANCHOR_TOP_MIDDLE ? width / 2 : kArrowX;
310    points.push_back(MakeBidiGdkPoint(
311        arrow_x - top_arrow_size + x_off_r, top_arrow_size, width, on_left));
312    points.push_back(MakeBidiGdkPoint(
313        arrow_x + x_off_r, 0, width, on_left));
314    points.push_back(MakeBidiGdkPoint(
315        arrow_x + 1 + x_off_l, 0, width, on_left));
316    points.push_back(MakeBidiGdkPoint(
317        arrow_x + top_arrow_size + 1 + x_off_l, top_arrow_size,
318        width, on_left));
319  }
320
321  // Top right corner.
322  points.push_back(MakeBidiGdkPoint(
323      width - kCornerSize + 1 + x_off_l, top_arrow_size, width, on_left));
324  points.push_back(MakeBidiGdkPoint(
325      width + x_off_l, top_arrow_size + kCornerSize - 1, width, on_left));
326
327  // Bottom right corner.
328  points.push_back(MakeBidiGdkPoint(
329      width + x_off_l, height - bottom_arrow_size - kCornerSize,
330      width, on_left));
331  points.push_back(MakeBidiGdkPoint(
332      width - kCornerSize + x_off_r, height - bottom_arrow_size + y_off,
333      width, on_left));
334
335  // The bottom arrow.
336  if (bottom_arrow_size) {
337    int arrow_x = frame_style == ANCHOR_BOTTOM_MIDDLE ?
338        width / 2 : kArrowX;
339    points.push_back(MakeBidiGdkPoint(
340        arrow_x + bottom_arrow_size + 1 + x_off_l,
341        height - bottom_arrow_size + y_off,
342        width,
343        on_left));
344    points.push_back(MakeBidiGdkPoint(
345        arrow_x + 1 + x_off_l, height + y_off, width, on_left));
346    points.push_back(MakeBidiGdkPoint(
347        arrow_x + x_off_r, height + y_off, width, on_left));
348    points.push_back(MakeBidiGdkPoint(
349        arrow_x - bottom_arrow_size + x_off_r,
350        height - bottom_arrow_size + y_off,
351        width,
352        on_left));
353  }
354
355  // Bottom left corner.
356  points.push_back(MakeBidiGdkPoint(
357      kCornerSize + x_off_l, height -bottom_arrow_size + y_off,
358      width, on_left));
359  points.push_back(MakeBidiGdkPoint(
360      x_off_r, height - bottom_arrow_size - kCornerSize, width, on_left));
361
362  return points;
363}
364
365BubbleGtk::FrameStyle BubbleGtk::GetAllowedFrameStyle(
366    FrameStyle preferred_style,
367    int arrow_x,
368    int arrow_y,
369    int width,
370    int height) {
371  if (IsFixed(preferred_style))
372    return preferred_style;
373
374  const int screen_width = gdk_screen_get_width(gdk_screen_get_default());
375  const int screen_height = gdk_screen_get_height(gdk_screen_get_default());
376
377  // Choose whether to show the bubble above or below the specified location.
378  const bool prefer_top_arrow = IsArrowTop(preferred_style) ||
379             preferred_style == FLOAT_BELOW_RECT;
380  // The bleed measures the amount of bubble that would be shown offscreen.
381  const int top_arrow_bleed =
382      std::max(height + kArrowSize + arrow_y - screen_height, 0);
383  const int bottom_arrow_bleed = std::max(height + kArrowSize - arrow_y, 0);
384
385  FrameStyle frame_style_none = FLOAT_BELOW_RECT;
386  FrameStyle frame_style_left = ANCHOR_TOP_LEFT;
387  FrameStyle frame_style_middle = ANCHOR_TOP_MIDDLE;
388  FrameStyle frame_style_right = ANCHOR_TOP_RIGHT;
389  if ((prefer_top_arrow && (top_arrow_bleed > bottom_arrow_bleed)) ||
390      (!prefer_top_arrow && (top_arrow_bleed >= bottom_arrow_bleed))) {
391    frame_style_none = CENTER_OVER_RECT;
392    frame_style_left = ANCHOR_BOTTOM_LEFT;
393    frame_style_middle = ANCHOR_BOTTOM_MIDDLE;
394    frame_style_right = ANCHOR_BOTTOM_RIGHT;
395  }
396
397  if (!HasArrow(preferred_style))
398    return frame_style_none;
399
400  if (IsArrowMiddle(preferred_style))
401    return frame_style_middle;
402
403  // Choose whether to show the bubble left or right of the specified location.
404  const bool prefer_left_arrow = IsArrowLeft(preferred_style);
405  // The bleed measures the amount of bubble that would be shown offscreen.
406  const int left_arrow_bleed =
407      std::max(width + arrow_x - kArrowX - screen_width, 0);
408  const int right_arrow_bleed = std::max(width - arrow_x - kArrowX, 0);
409
410  // Use the preferred location if it doesn't bleed more than the opposite side.
411  return ((prefer_left_arrow && (left_arrow_bleed <= right_arrow_bleed)) ||
412          (!prefer_left_arrow && (left_arrow_bleed < right_arrow_bleed))) ?
413      frame_style_left : frame_style_right;
414}
415
416bool BubbleGtk::UpdateFrameStyle(bool force_move_and_reshape) {
417  if (!toplevel_window_ || !anchor_widget_)
418    return false;
419
420  gint toplevel_x = 0, toplevel_y = 0;
421  gdk_window_get_position(gtk_widget_get_window(toplevel_window_),
422                          &toplevel_x, &toplevel_y);
423  int offset_x, offset_y;
424  gtk_widget_translate_coordinates(anchor_widget_, toplevel_window_,
425                                   rect_.x(), rect_.y(), &offset_x, &offset_y);
426
427  FrameStyle old_frame_style = actual_frame_style_;
428  GtkAllocation allocation;
429  gtk_widget_get_allocation(window_, &allocation);
430  actual_frame_style_ = GetAllowedFrameStyle(
431      requested_frame_style_,
432      toplevel_x + offset_x + (rect_.width() / 2),  // arrow_x
433      toplevel_y + offset_y,
434      allocation.width,
435      allocation.height);
436
437  if (force_move_and_reshape || actual_frame_style_ != old_frame_style) {
438    UpdateWindowShape();
439    MoveWindow();
440    // We need to redraw the entire window to repaint its border.
441    gtk_widget_queue_draw(window_);
442    return true;
443  }
444  return false;
445}
446
447void BubbleGtk::UpdateWindowShape() {
448  if (mask_region_) {
449    gdk_region_destroy(mask_region_);
450    mask_region_ = NULL;
451  }
452  GtkAllocation allocation;
453  gtk_widget_get_allocation(window_, &allocation);
454  std::vector<GdkPoint> points = MakeFramePolygonPoints(
455      actual_frame_style_, allocation.width, allocation.height,
456      FRAME_MASK);
457  mask_region_ = gdk_region_polygon(&points[0],
458                                    points.size(),
459                                    GDK_EVEN_ODD_RULE);
460
461  GdkWindow* gdk_window = gtk_widget_get_window(window_);
462  gdk_window_shape_combine_region(gdk_window, NULL, 0, 0);
463  gdk_window_shape_combine_region(gdk_window, mask_region_, 0, 0);
464}
465
466void BubbleGtk::MoveWindow() {
467  if (!toplevel_window_ || !anchor_widget_)
468    return;
469
470  gint toplevel_x = 0, toplevel_y = 0;
471  gdk_window_get_position(gtk_widget_get_window(toplevel_window_),
472                          &toplevel_x, &toplevel_y);
473
474  int offset_x, offset_y;
475  gtk_widget_translate_coordinates(anchor_widget_, toplevel_window_,
476                                   rect_.x(), rect_.y(), &offset_x, &offset_y);
477
478  gint screen_x = 0;
479  if (IsFixed(actual_frame_style_)) {
480    GtkAllocation toplevel_allocation;
481    gtk_widget_get_allocation(toplevel_window_, &toplevel_allocation);
482
483    GtkAllocation bubble_allocation;
484    gtk_widget_get_allocation(window_, &bubble_allocation);
485
486    int x_offset = actual_frame_style_ == FIXED_TOP_LEFT ?
487        kFixedPositionPaddingEnd :
488        toplevel_allocation.width - bubble_allocation.width -
489            kFixedPositionPaddingEnd;
490    screen_x = toplevel_x + x_offset;
491  } else if (!HasArrow(actual_frame_style_) ||
492             IsArrowMiddle(actual_frame_style_)) {
493    GtkAllocation allocation;
494    gtk_widget_get_allocation(window_, &allocation);
495    screen_x =
496        toplevel_x + offset_x + (rect_.width() / 2) - allocation.width / 2;
497  } else if (IsArrowLeft(actual_frame_style_)) {
498    screen_x = toplevel_x + offset_x + (rect_.width() / 2) - kArrowX;
499  } else if (IsArrowRight(actual_frame_style_)) {
500    GtkAllocation allocation;
501    gtk_widget_get_allocation(window_, &allocation);
502    screen_x = toplevel_x + offset_x + (rect_.width() / 2) -
503               allocation.width + kArrowX;
504  } else {
505    NOTREACHED();
506  }
507
508  gint screen_y = toplevel_y + offset_y + rect_.height();
509  if (IsFixed(actual_frame_style_)) {
510    screen_y = toplevel_y + kFixedPositionPaddingTop;
511  } else if (IsArrowTop(actual_frame_style_) ||
512             actual_frame_style_ == FLOAT_BELOW_RECT) {
513    screen_y += kArrowToContentPadding;
514  } else {
515    GtkAllocation allocation;
516    gtk_widget_get_allocation(window_, &allocation);
517    screen_y -= allocation.height + kArrowToContentPadding;
518  }
519
520  gtk_window_move(GTK_WINDOW(window_), screen_x, screen_y);
521}
522
523void BubbleGtk::StackWindow() {
524  // Stack our window directly above the toplevel window.
525  if (toplevel_window_)
526    ui::StackPopupWindow(window_, toplevel_window_);
527}
528
529void BubbleGtk::Observe(int type,
530                        const content::NotificationSource& source,
531                        const content::NotificationDetails& details) {
532  DCHECK_EQ(type, chrome::NOTIFICATION_BROWSER_THEME_CHANGED);
533  if (theme_service_->UsingNativeTheme() && match_system_theme_) {
534    gtk_widget_modify_bg(window_, GTK_STATE_NORMAL, NULL);
535  } else {
536    // Set the background color, so we don't need to paint it manually.
537    gtk_widget_modify_bg(window_, GTK_STATE_NORMAL, &kBackgroundColor);
538  }
539}
540
541void BubbleGtk::StopGrabbingInput() {
542  UngrabPointerAndKeyboard();
543  if (!grab_input_)
544    return;
545  grab_input_ = false;
546  gtk_grab_remove(window_);
547}
548
549GtkWindow* BubbleGtk::GetNativeWindow() {
550  return GTK_WINDOW(window_);
551}
552
553void BubbleGtk::Close() {
554  // We don't need to ungrab the pointer or keyboard here; the X server will
555  // automatically do that when we destroy our window.
556  DCHECK(window_);
557  gtk_widget_destroy(window_);
558  // |this| has been deleted, see OnDestroy.
559}
560
561void BubbleGtk::SetPositionRelativeToAnchor(const gfx::Rect* rect) {
562  rect_ = rect ? *rect : gtk_util::WidgetBounds(anchor_widget_);
563  if (!UpdateFrameStyle(false))
564    MoveWindow();
565}
566
567void BubbleGtk::GrabPointerAndKeyboard() {
568  GdkWindow* gdk_window = gtk_widget_get_window(window_);
569
570  // Install X pointer and keyboard grabs to make sure that we have the focus
571  // and get all mouse and keyboard events until we're closed. As a hack, grab
572  // the pointer even if |grab_input_| is false to prevent a weird error
573  // rendering the bubble's frame. See
574  // https://code.google.com/p/chromium/issues/detail?id=130820.
575  GdkGrabStatus pointer_grab_status =
576      gdk_pointer_grab(gdk_window,
577                       TRUE,                   // owner_events
578                       GDK_BUTTON_PRESS_MASK,  // event_mask
579                       NULL,                   // confine_to
580                       NULL,                   // cursor
581                       GDK_CURRENT_TIME);
582  if (pointer_grab_status != GDK_GRAB_SUCCESS) {
583    // This will fail if someone else already has the pointer grabbed, but
584    // there's not really anything we can do about that.
585    DLOG(ERROR) << "Unable to grab pointer (status="
586                << pointer_grab_status << ")";
587  }
588
589  // Only grab the keyboard input if |grab_input_| is true.
590  if (grab_input_) {
591    GdkGrabStatus keyboard_grab_status =
592        gdk_keyboard_grab(gdk_window,
593                          FALSE,  // owner_events
594                          GDK_CURRENT_TIME);
595    if (keyboard_grab_status != GDK_GRAB_SUCCESS) {
596      DLOG(ERROR) << "Unable to grab keyboard (status="
597                  << keyboard_grab_status << ")";
598    }
599  }
600}
601
602void BubbleGtk::UngrabPointerAndKeyboard() {
603  gdk_pointer_ungrab(GDK_CURRENT_TIME);
604  if (grab_input_)
605    gdk_keyboard_ungrab(GDK_CURRENT_TIME);
606}
607
608gboolean BubbleGtk::OnGtkAccelerator(GtkAccelGroup* group,
609                                     GObject* acceleratable,
610                                     guint keyval,
611                                     GdkModifierType modifier) {
612  GdkEventKey msg;
613  GdkKeymapKey* keys;
614  gint n_keys;
615
616  switch (keyval) {
617    case GDK_Escape:
618      // Close on Esc and trap the accelerator
619      closed_by_escape_ = true;
620      Close();
621      return TRUE;
622    case GDK_w:
623      // Close on C-w and forward the accelerator
624      if (modifier & GDK_CONTROL_MASK) {
625        gdk_keymap_get_entries_for_keyval(NULL,
626                                          keyval,
627                                          &keys,
628                                          &n_keys);
629        if (n_keys) {
630          // Forward the accelerator to root window the bubble is anchored
631          // to for further processing
632          msg.type = GDK_KEY_PRESS;
633          msg.window = gtk_widget_get_window(toplevel_window_);
634          msg.send_event = TRUE;
635          msg.time = GDK_CURRENT_TIME;
636          msg.state = modifier | GDK_MOD2_MASK;
637          msg.keyval = keyval;
638          // length and string are deprecated and thus zeroed out
639          msg.length = 0;
640          msg.string = NULL;
641          msg.hardware_keycode = keys[0].keycode;
642          msg.group = keys[0].group;
643          msg.is_modifier = 0;
644
645          g_free(keys);
646
647          gtk_main_do_event(reinterpret_cast<GdkEvent*>(&msg));
648        } else {
649          // This means that there isn't a h/w code for the keyval in the
650          // current keymap, which is weird but possible if the keymap just
651          // changed. This isn't a critical error, but might be indicative
652          // of something off if it happens regularly.
653          DLOG(WARNING) << "Found no keys for value " << keyval;
654        }
655        Close();
656      }
657      break;
658    default:
659      return FALSE;
660  }
661
662  return TRUE;
663}
664
665gboolean BubbleGtk::OnExpose(GtkWidget* widget, GdkEventExpose* expose) {
666  // TODO(erg): This whole method will need to be rewritten in cairo.
667  GdkDrawable* drawable = GDK_DRAWABLE(gtk_widget_get_window(window_));
668  GdkGC* gc = gdk_gc_new(drawable);
669  gdk_gc_set_rgb_fg_color(gc, &kFrameColor);
670
671  // Stroke the frame border.
672  GtkAllocation allocation;
673  gtk_widget_get_allocation(window_, &allocation);
674  std::vector<GdkPoint> points = MakeFramePolygonPoints(
675      actual_frame_style_, allocation.width, allocation.height,
676      FRAME_STROKE);
677  gdk_draw_polygon(drawable, gc, FALSE, &points[0], points.size());
678
679  // If |grab_input_| is false, pointer input has been grabbed as a hack in
680  // |GrabPointerAndKeyboard()| to ensure that the polygon frame is drawn
681  // correctly. Since the intention is not actually to grab the pointer, release
682  // it now that the frame is drawn to prevent clicks from being missed. See
683  // https://code.google.com/p/chromium/issues/detail?id=130820.
684  if (!grab_input_)
685    gdk_pointer_ungrab(GDK_CURRENT_TIME);
686
687  g_object_unref(gc);
688  return FALSE;  // Propagate so our children paint, etc.
689}
690
691// When our size is initially allocated or changed, we need to recompute
692// and apply our shape mask region.
693void BubbleGtk::OnSizeAllocate(GtkWidget* widget,
694                               GtkAllocation* allocation) {
695  if (!UpdateFrameStyle(false)) {
696    UpdateWindowShape();
697    MoveWindow();
698  }
699}
700
701gboolean BubbleGtk::OnButtonPress(GtkWidget* widget,
702                                  GdkEventButton* event) {
703  // If we got a click in our own window, that's okay (we need to additionally
704  // check that it falls within our bounds, since we've grabbed the pointer and
705  // some events that actually occurred in other windows will be reported with
706  // respect to our window).
707  GdkWindow* gdk_window = gtk_widget_get_window(window_);
708  if (event->window == gdk_window &&
709      (mask_region_ && gdk_region_point_in(mask_region_, event->x, event->y))) {
710    return FALSE;  // Propagate.
711  }
712
713  // Our content widget got a click.
714  if (event->window != gdk_window &&
715      gdk_window_get_toplevel(event->window) == gdk_window) {
716    return FALSE;
717  }
718
719  if (grab_input_) {
720    // Otherwise we had a click outside of our window, close ourself.
721    Close();
722    return TRUE;
723  }
724
725  return FALSE;
726}
727
728gboolean BubbleGtk::OnDestroy(GtkWidget* widget) {
729  // We are self deleting, we have a destroy signal setup to catch when we
730  // destroy the widget manually, or the window was closed via X.  This will
731  // delete the BubbleGtk object.
732  delete this;
733  return FALSE;  // Propagate.
734}
735
736void BubbleGtk::OnHide(GtkWidget* widget) {
737  gtk_widget_destroy(widget);
738}
739
740gboolean BubbleGtk::OnGrabBroken(GtkWidget* widget,
741                                 GdkEventGrabBroken* grab_broken) {
742  // |grab_input_| may have been changed to false.
743  if (!grab_input_)
744    return FALSE;
745
746  // |grab_window| can be NULL.
747  if (!grab_broken->grab_window)
748    return FALSE;
749
750  gpointer user_data;
751  gdk_window_get_user_data(grab_broken->grab_window, &user_data);
752
753  if (GTK_IS_WIDGET(user_data)) {
754    signals_.Connect(GTK_WIDGET(user_data), "hide",
755                     G_CALLBACK(OnForeshadowWidgetHideThunk), this);
756  }
757
758  return FALSE;
759}
760
761void BubbleGtk::OnForeshadowWidgetHide(GtkWidget* widget) {
762  if (grab_input_)
763    GrabPointerAndKeyboard();
764
765  signals_.DisconnectAll(widget);
766}
767
768gboolean BubbleGtk::OnToplevelConfigure(GtkWidget* widget,
769                                        GdkEventConfigure* event) {
770  if (!UpdateFrameStyle(false))
771    MoveWindow();
772  StackWindow();
773  return FALSE;
774}
775
776gboolean BubbleGtk::OnToplevelUnmap(GtkWidget* widget, GdkEvent* event) {
777  Close();
778  return FALSE;
779}
780
781void BubbleGtk::OnAnchorAllocate(GtkWidget* widget,
782                                 GtkAllocation* allocation) {
783  if (!UpdateFrameStyle(false))
784    MoveWindow();
785}
786
787void BubbleGtk::OnAnchorDestroy(GtkWidget* widget) {
788  anchor_widget_ = NULL;
789  Close();
790  // |this| is deleted.
791}
792
793void BubbleGtk::OnToplevelDestroy(GtkWidget* widget) {
794  toplevel_window_ = NULL;
795  Close();
796  // |this| is deleted.
797}
798