• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/bind.h"
10 #include "base/i18n/rtl.h"
11 #include "base/message_loop/message_loop.h"
12 #include "chrome/browser/chrome_notification_types.h"
13 #include "chrome/browser/ui/gtk/bubble/bubble_accelerators_gtk.h"
14 #include "chrome/browser/ui/gtk/gtk_theme_service.h"
15 #include "chrome/browser/ui/gtk/gtk_util.h"
16 #include "content/public/browser/notification_source.h"
17 #include "ui/base/gtk/gtk_hig_constants.h"
18 #include "ui/base/gtk/gtk_windowing.h"
19 #include "ui/gfx/gtk_compat.h"
20 #include "ui/gfx/gtk_util.h"
21 #include "ui/gfx/path.h"
22 #include "ui/gfx/rect.h"
23 
24 namespace {
25 
26 // The height of the arrow, and the width will be about twice the height.
27 const int kArrowSize = 8;
28 
29 // Number of pixels to the middle of the arrow from the close edge of the
30 // window.
31 const int kArrowX = 18;
32 
33 // Number of pixels between the tip of the arrow and the region we're
34 // pointing to.
35 const int kArrowToContentPadding = -4;
36 
37 // We draw flat diagonal corners, each corner is an NxN square.
38 const int kCornerSize = 3;
39 
40 // The amount of padding (in pixels) from the top of |toplevel_window_| to the
41 // top of |window_| when fixed positioning is used.
42 const int kFixedPositionPaddingEnd = 10;
43 const int kFixedPositionPaddingTop = 5;
44 
45 const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xff, 0xff, 0xff);
46 const GdkColor kFrameColor = GDK_COLOR_RGB(0x63, 0x63, 0x63);
47 
48 // Helper functions that encapsulate arrow locations.
HasArrow(BubbleGtk::FrameStyle frame_style)49 bool HasArrow(BubbleGtk::FrameStyle frame_style) {
50   return frame_style != BubbleGtk::FLOAT_BELOW_RECT &&
51          frame_style != BubbleGtk::CENTER_OVER_RECT &&
52          frame_style != BubbleGtk::FIXED_TOP_LEFT &&
53          frame_style != BubbleGtk::FIXED_TOP_RIGHT;
54 }
55 
IsArrowLeft(BubbleGtk::FrameStyle frame_style)56 bool IsArrowLeft(BubbleGtk::FrameStyle frame_style) {
57   return frame_style == BubbleGtk::ANCHOR_TOP_LEFT ||
58          frame_style == BubbleGtk::ANCHOR_BOTTOM_LEFT;
59 }
60 
IsArrowMiddle(BubbleGtk::FrameStyle frame_style)61 bool IsArrowMiddle(BubbleGtk::FrameStyle frame_style) {
62   return frame_style == BubbleGtk::ANCHOR_TOP_MIDDLE ||
63          frame_style == BubbleGtk::ANCHOR_BOTTOM_MIDDLE;
64 }
65 
IsArrowRight(BubbleGtk::FrameStyle frame_style)66 bool IsArrowRight(BubbleGtk::FrameStyle frame_style) {
67   return frame_style == BubbleGtk::ANCHOR_TOP_RIGHT ||
68          frame_style == BubbleGtk::ANCHOR_BOTTOM_RIGHT;
69 }
70 
IsArrowTop(BubbleGtk::FrameStyle frame_style)71 bool IsArrowTop(BubbleGtk::FrameStyle frame_style) {
72   return frame_style == BubbleGtk::ANCHOR_TOP_LEFT ||
73          frame_style == BubbleGtk::ANCHOR_TOP_MIDDLE ||
74          frame_style == BubbleGtk::ANCHOR_TOP_RIGHT;
75 }
76 
IsArrowBottom(BubbleGtk::FrameStyle frame_style)77 bool IsArrowBottom(BubbleGtk::FrameStyle frame_style) {
78   return frame_style == BubbleGtk::ANCHOR_BOTTOM_LEFT ||
79          frame_style == BubbleGtk::ANCHOR_BOTTOM_MIDDLE ||
80          frame_style == BubbleGtk::ANCHOR_BOTTOM_RIGHT;
81 }
82 
IsFixed(BubbleGtk::FrameStyle frame_style)83 bool IsFixed(BubbleGtk::FrameStyle frame_style) {
84   return frame_style == BubbleGtk::FIXED_TOP_LEFT ||
85          frame_style == BubbleGtk::FIXED_TOP_RIGHT;
86 }
87 
AdjustFrameStyleForLocale(BubbleGtk::FrameStyle frame_style)88 BubbleGtk::FrameStyle AdjustFrameStyleForLocale(
89     BubbleGtk::FrameStyle frame_style) {
90   // Only RTL requires more work.
91   if (!base::i18n::IsRTL())
92     return frame_style;
93 
94   switch (frame_style) {
95     // These don't flip.
96     case BubbleGtk::ANCHOR_TOP_MIDDLE:
97     case BubbleGtk::ANCHOR_BOTTOM_MIDDLE:
98     case BubbleGtk::FLOAT_BELOW_RECT:
99     case BubbleGtk::CENTER_OVER_RECT:
100       return frame_style;
101 
102     // These do flip.
103     case BubbleGtk::ANCHOR_TOP_LEFT:
104       return BubbleGtk::ANCHOR_TOP_RIGHT;
105 
106     case BubbleGtk::ANCHOR_TOP_RIGHT:
107       return BubbleGtk::ANCHOR_TOP_LEFT;
108 
109     case BubbleGtk::ANCHOR_BOTTOM_LEFT:
110       return BubbleGtk::ANCHOR_BOTTOM_RIGHT;
111 
112     case BubbleGtk::ANCHOR_BOTTOM_RIGHT:
113       return BubbleGtk::ANCHOR_BOTTOM_LEFT;
114 
115     case BubbleGtk::FIXED_TOP_LEFT:
116       return BubbleGtk::FIXED_TOP_RIGHT;
117 
118     case BubbleGtk::FIXED_TOP_RIGHT:
119       return BubbleGtk::FIXED_TOP_LEFT;
120   }
121 
122   NOTREACHED();
123   return BubbleGtk::ANCHOR_TOP_LEFT;
124 }
125 
126 }  // namespace
127 
128 // static
Show(GtkWidget * anchor_widget,const gfx::Rect * rect,GtkWidget * content,FrameStyle frame_style,int attribute_flags,GtkThemeService * provider,BubbleDelegateGtk * delegate)129 BubbleGtk* BubbleGtk::Show(GtkWidget* anchor_widget,
130                            const gfx::Rect* rect,
131                            GtkWidget* content,
132                            FrameStyle frame_style,
133                            int attribute_flags,
134                            GtkThemeService* provider,
135                            BubbleDelegateGtk* delegate) {
136   BubbleGtk* bubble = new BubbleGtk(provider,
137                                     AdjustFrameStyleForLocale(frame_style),
138                                     attribute_flags);
139   bubble->Init(anchor_widget, rect, content, attribute_flags);
140   bubble->set_delegate(delegate);
141   return bubble;
142 }
143 
BubbleGtk(GtkThemeService * provider,FrameStyle frame_style,int attribute_flags)144 BubbleGtk::BubbleGtk(GtkThemeService* provider,
145                      FrameStyle frame_style,
146                      int attribute_flags)
147     : delegate_(NULL),
148       window_(NULL),
149       theme_service_(provider),
150       accel_group_(gtk_accel_group_new()),
151       toplevel_window_(NULL),
152       anchor_widget_(NULL),
153       mask_region_(NULL),
154       requested_frame_style_(frame_style),
155       actual_frame_style_(ANCHOR_TOP_LEFT),
156       match_system_theme_(attribute_flags & MATCH_SYSTEM_THEME),
157       grab_input_(attribute_flags & GRAB_INPUT),
158       closed_by_escape_(false),
159       weak_ptr_factory_(this) {}
160 
~BubbleGtk()161 BubbleGtk::~BubbleGtk() {
162   // Notify the delegate that we're about to close.  This gives the chance
163   // to save state / etc from the hosted widget before it's destroyed.
164   if (delegate_)
165     delegate_->BubbleClosing(this, closed_by_escape_);
166 
167   g_object_unref(accel_group_);
168   if (mask_region_)
169     gdk_region_destroy(mask_region_);
170 }
171 
Init(GtkWidget * anchor_widget,const gfx::Rect * rect,GtkWidget * content,int attribute_flags)172 void BubbleGtk::Init(GtkWidget* anchor_widget,
173                      const gfx::Rect* rect,
174                      GtkWidget* content,
175                      int attribute_flags) {
176   // If there is a current grab widget (menu, other bubble, etc.), hide it.
177   GtkWidget* current_grab_widget = gtk_grab_get_current();
178   if (current_grab_widget)
179     gtk_widget_hide(current_grab_widget);
180 
181   DCHECK(!window_);
182   anchor_widget_ = anchor_widget;
183   toplevel_window_ = gtk_widget_get_toplevel(anchor_widget_);
184   DCHECK(gtk_widget_is_toplevel(toplevel_window_));
185   rect_ = rect ? *rect : gtk_util::WidgetBounds(anchor_widget);
186 
187   // Using a TOPLEVEL window may cause placement issues with certain WMs but it
188   // is necessary to be able to focus the window.
189   window_ = gtk_window_new(attribute_flags & POPUP_WINDOW ?
190                            GTK_WINDOW_POPUP : GTK_WINDOW_TOPLEVEL);
191 
192   gtk_widget_set_app_paintable(window_, TRUE);
193   // Resizing is handled by the program, not user.
194   gtk_window_set_resizable(GTK_WINDOW(window_), FALSE);
195 
196   if (!(attribute_flags & NO_ACCELERATORS)) {
197     // Attach all of the accelerators to the bubble.
198     for (BubbleAcceleratorsGtk::const_iterator
199              i(BubbleAcceleratorsGtk::begin());
200          i != BubbleAcceleratorsGtk::end();
201          ++i) {
202       gtk_accel_group_connect(accel_group_,
203                               i->keyval,
204                               i->modifier_type,
205                               GtkAccelFlags(0),
206                               g_cclosure_new(G_CALLBACK(&OnGtkAcceleratorThunk),
207                                              this,
208                                              NULL));
209     }
210     gtk_window_add_accel_group(GTK_WINDOW(window_), accel_group_);
211   }
212 
213   // |requested_frame_style_| is used instead of |actual_frame_style_| here
214   // because |actual_frame_style_| is only correct after calling
215   // |UpdateFrameStyle()|. Unfortunately, |UpdateFrameStyle()| requires knowing
216   // the size of |window_| (which happens later on).
217   int arrow_padding = HasArrow(requested_frame_style_) ? kArrowSize : 0;
218   GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
219   gtk_alignment_set_padding(GTK_ALIGNMENT(alignment), arrow_padding, 0, 0, 0);
220 
221   gtk_container_add(GTK_CONTAINER(alignment), content);
222   gtk_container_add(GTK_CONTAINER(window_), alignment);
223 
224   // GtkWidget only exposes the bitmap mask interface.  Use GDK to more
225   // efficently mask a GdkRegion.  Make sure the window is realized during
226   // OnSizeAllocate, so the mask can be applied to the GdkWindow.
227   gtk_widget_realize(window_);
228 
229   UpdateFrameStyle(true);  // Force move and reshape.
230   StackWindow();
231 
232   gtk_widget_add_events(window_, GDK_BUTTON_PRESS_MASK);
233 
234   // Connect during the bubbling phase so the border is always on top.
235   signals_.ConnectAfter(window_, "expose-event",
236                         G_CALLBACK(OnExposeThunk), this);
237   signals_.Connect(window_, "size-allocate", G_CALLBACK(OnSizeAllocateThunk),
238                    this);
239   signals_.Connect(window_, "button-press-event",
240                    G_CALLBACK(OnButtonPressThunk), this);
241   signals_.Connect(window_, "destroy", G_CALLBACK(OnDestroyThunk), this);
242   signals_.Connect(window_, "hide", G_CALLBACK(OnHideThunk), this);
243   if (grab_input_) {
244     signals_.Connect(window_, "grab-broken-event",
245                      G_CALLBACK(OnGrabBrokenThunk), this);
246   }
247 
248   signals_.Connect(anchor_widget_, "destroy",
249                    G_CALLBACK(OnAnchorDestroyThunk), this);
250   // If the toplevel window is being used as the anchor, then the signals below
251   // are enough to keep us positioned correctly.
252   if (anchor_widget_ != toplevel_window_) {
253     signals_.Connect(anchor_widget_, "size-allocate",
254                      G_CALLBACK(OnAnchorAllocateThunk), this);
255   }
256 
257   signals_.Connect(toplevel_window_, "configure-event",
258                    G_CALLBACK(OnToplevelConfigureThunk), this);
259   signals_.Connect(toplevel_window_, "unmap-event",
260                    G_CALLBACK(OnToplevelUnmapThunk), 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
MakeFramePolygonPoints(FrameStyle frame_style,int width,int height,FrameType type)279 std::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 
GetAllowedFrameStyle(FrameStyle preferred_style,int arrow_x,int arrow_y,int width,int height)365 BubbleGtk::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 
UpdateFrameStyle(bool force_move_and_reshape)416 bool 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 
UpdateWindowShape()447 void 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 
MoveWindow()466 void 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 
StackWindow()523 void BubbleGtk::StackWindow() {
524   // Stack our window directly above the toplevel window.
525   if (toplevel_window_)
526     ui::StackPopupWindow(window_, toplevel_window_);
527 }
528 
Observe(int type,const content::NotificationSource & source,const content::NotificationDetails & details)529 void 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 
StopGrabbingInput()541 void BubbleGtk::StopGrabbingInput() {
542   UngrabPointerAndKeyboard();
543   if (!grab_input_)
544     return;
545   grab_input_ = false;
546   gtk_grab_remove(window_);
547 }
548 
GetNativeWindow()549 GtkWindow* BubbleGtk::GetNativeWindow() {
550   return GTK_WINDOW(window_);
551 }
552 
Close()553 void 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 
SetPositionRelativeToAnchor(const gfx::Rect * rect)561 void BubbleGtk::SetPositionRelativeToAnchor(const gfx::Rect* rect) {
562   rect_ = rect ? *rect : gtk_util::WidgetBounds(anchor_widget_);
563   if (!UpdateFrameStyle(false))
564     MoveWindow();
565 }
566 
GrabPointerAndKeyboard()567 void 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 
UngrabPointerAndKeyboard()602 void BubbleGtk::UngrabPointerAndKeyboard() {
603   gdk_pointer_ungrab(GDK_CURRENT_TIME);
604   if (grab_input_)
605     gdk_keyboard_ungrab(GDK_CURRENT_TIME);
606 }
607 
OnGtkAccelerator(GtkAccelGroup * group,GObject * acceleratable,guint keyval,GdkModifierType modifier)608 gboolean 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 
OnExpose(GtkWidget * widget,GdkEventExpose * expose)665 gboolean 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.
OnSizeAllocate(GtkWidget * widget,GtkAllocation * allocation)693 void BubbleGtk::OnSizeAllocate(GtkWidget* widget,
694                                GtkAllocation* allocation) {
695   if (!UpdateFrameStyle(false)) {
696     UpdateWindowShape();
697     MoveWindow();
698   }
699 }
700 
OnButtonPress(GtkWidget * widget,GdkEventButton * event)701 gboolean 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 
OnDestroy(GtkWidget * widget)728 gboolean 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 
OnHide(GtkWidget * widget)736 void BubbleGtk::OnHide(GtkWidget* widget) {
737   gtk_widget_destroy(widget);
738 }
739 
OnGrabBroken(GtkWidget * widget,GdkEventGrabBroken * grab_broken)740 gboolean 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 
OnForeshadowWidgetHide(GtkWidget * widget)761 void BubbleGtk::OnForeshadowWidgetHide(GtkWidget* widget) {
762   if (grab_input_)
763     GrabPointerAndKeyboard();
764 
765   signals_.DisconnectAll(widget);
766 }
767 
OnToplevelConfigure(GtkWidget * widget,GdkEventConfigure * event)768 gboolean BubbleGtk::OnToplevelConfigure(GtkWidget* widget,
769                                         GdkEventConfigure* event) {
770   if (!UpdateFrameStyle(false))
771     MoveWindow();
772   StackWindow();
773   return FALSE;
774 }
775 
OnToplevelUnmap(GtkWidget * widget,GdkEvent * event)776 gboolean BubbleGtk::OnToplevelUnmap(GtkWidget* widget, GdkEvent* event) {
777   Close();
778   return FALSE;
779 }
780 
OnAnchorAllocate(GtkWidget * widget,GtkAllocation * allocation)781 void BubbleGtk::OnAnchorAllocate(GtkWidget* widget,
782                                  GtkAllocation* allocation) {
783   if (!UpdateFrameStyle(false))
784     MoveWindow();
785 }
786 
OnAnchorDestroy(GtkWidget * widget)787 void BubbleGtk::OnAnchorDestroy(GtkWidget* widget) {
788   anchor_widget_ = NULL;
789 
790   // Ctrl-W will first destroy the anchor, then call |this| back via
791   // |accel_group_|. So that the callback |this| registered via |accel_group_|
792   // doesn't run after |this| is destroyed, we delay this close (which, unlike
793   // the accelerator callback, will be cancelled if |this| is destroyed).
794   // http://crbug.com/286621
795   base::MessageLoop::current()->PostTask(
796       FROM_HERE,
797       base::Bind(&BubbleGtk::Close, weak_ptr_factory_.GetWeakPtr()));
798 }
799