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