blob: 8e7ae22f857a0ad39c4556a3bb319b4b7eb07f87 [file] [log] [blame]
// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/gtk/info_bubble_gtk.h"
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>
#include "app/gfx/gtk_util.h"
#include "app/gfx/path.h"
#include "app/l10n_util.h"
#include "base/basictypes.h"
#include "base/gfx/rect.h"
#include "base/logging.h"
#include "chrome/browser/gtk/gtk_theme_provider.h"
#include "chrome/common/gtk_util.h"
#include "chrome/common/notification_service.h"
namespace {
// The height of the arrow, and the width will be about twice the height.
const int kArrowSize = 5;
// Number of pixels to the start of the arrow from the edge of the window.
const int kArrowX = 13;
// Number of pixels between the tip of the arrow and the region we're
// pointing to.
const int kArrowToContentPadding = -6;
// We draw flat diagonal corners, each corner is an NxN square.
const int kCornerSize = 3;
// Margins around the content.
const int kTopMargin = kArrowSize + kCornerSize + 6;
const int kBottomMargin = kCornerSize + 6;
const int kLeftMargin = kCornerSize + 6;
const int kRightMargin = kCornerSize + 6;
const GdkColor kBackgroundColor = GDK_COLOR_RGB(0xff, 0xff, 0xff);
const GdkColor kFrameColor = GDK_COLOR_RGB(0x63, 0x63, 0x63);
} // namespace
// static
InfoBubbleGtk* InfoBubbleGtk::Show(GtkWindow* toplevel_window,
const gfx::Rect& rect,
GtkWidget* content,
ArrowLocationGtk arrow_location,
bool match_system_theme,
GtkThemeProvider* provider,
InfoBubbleGtkDelegate* delegate) {
InfoBubbleGtk* bubble = new InfoBubbleGtk(provider, match_system_theme);
bubble->Init(toplevel_window, rect, content, arrow_location);
bubble->set_delegate(delegate);
return bubble;
}
InfoBubbleGtk::InfoBubbleGtk(GtkThemeProvider* provider,
bool match_system_theme)
: delegate_(NULL),
window_(NULL),
theme_provider_(provider),
accel_group_(gtk_accel_group_new()),
toplevel_window_(NULL),
mask_region_(NULL),
preferred_arrow_location_(ARROW_LOCATION_TOP_LEFT),
current_arrow_location_(ARROW_LOCATION_TOP_LEFT),
match_system_theme_(match_system_theme) {
}
InfoBubbleGtk::~InfoBubbleGtk() {
g_object_unref(accel_group_);
if (mask_region_) {
gdk_region_destroy(mask_region_);
mask_region_ = NULL;
}
g_signal_handlers_disconnect_by_func(
toplevel_window_,
reinterpret_cast<gpointer>(HandleToplevelConfigureThunk),
this);
g_signal_handlers_disconnect_by_func(
toplevel_window_,
reinterpret_cast<gpointer>(HandleToplevelUnmapThunk),
this);
toplevel_window_ = NULL;
}
void InfoBubbleGtk::Init(GtkWindow* toplevel_window,
const gfx::Rect& rect,
GtkWidget* content,
ArrowLocationGtk arrow_location) {
DCHECK(!window_);
toplevel_window_ = toplevel_window;
rect_ = rect;
preferred_arrow_location_ = arrow_location;
window_ = gtk_window_new(GTK_WINDOW_POPUP);
gtk_widget_set_app_paintable(window_, TRUE);
// Resizing is handled by the program, not user.
gtk_window_set_resizable(GTK_WINDOW(window_), FALSE);
// Attach our accelerator group to the window with an escape accelerator.
gtk_accel_group_connect(accel_group_, GDK_Escape,
static_cast<GdkModifierType>(0), static_cast<GtkAccelFlags>(0),
g_cclosure_new(G_CALLBACK(&HandleEscapeThunk), this, NULL));
gtk_window_add_accel_group(GTK_WINDOW(window_), accel_group_);
GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
gtk_alignment_set_padding(GTK_ALIGNMENT(alignment),
kTopMargin, kBottomMargin,
kLeftMargin, kRightMargin);
gtk_container_add(GTK_CONTAINER(alignment), content);
gtk_container_add(GTK_CONTAINER(window_), alignment);
// GtkWidget only exposes the bitmap mask interface. Use GDK to more
// efficently mask a GdkRegion. Make sure the window is realized during
// HandleSizeAllocate, so the mask can be applied to the GdkWindow.
gtk_widget_realize(window_);
UpdateArrowLocation(true); // Force move and reshape.
StackWindow();
gtk_widget_add_events(window_, GDK_BUTTON_PRESS_MASK |
GDK_BUTTON_RELEASE_MASK);
g_signal_connect(window_, "expose-event",
G_CALLBACK(HandleExposeThunk), this);
g_signal_connect(window_, "size-allocate",
G_CALLBACK(HandleSizeAllocateThunk), this);
g_signal_connect(window_, "button-press-event",
G_CALLBACK(&HandleButtonPressThunk), this);
g_signal_connect(window_, "destroy",
G_CALLBACK(&HandleDestroyThunk), this);
g_signal_connect(toplevel_window, "configure-event",
G_CALLBACK(&HandleToplevelConfigureThunk), this);
g_signal_connect(toplevel_window, "unmap-event",
G_CALLBACK(&HandleToplevelUnmapThunk), this);
gtk_widget_show_all(window_);
// We add a GTK (application-level) grab. This means we will get all
// mouse events for our application, even if they were delivered on another
// window. We don't need this to get button presses outside of the bubble's
// window so we'll know to close it (the pointer grab takes care of that), but
// it prevents other widgets from getting highlighted when the pointer moves
// over them.
//
// (Ideally we wouldn't add the window to a group and it would just get all
// the mouse events, but gtk_grab_add() doesn't appear to do anything in that
// case. Adding it to the toplevel window's group first appears to block
// enter/leave events for that window and its subwindows, although other
// browser windows still receive them).
gtk_window_group_add_window(gtk_window_get_group(toplevel_window),
GTK_WINDOW(window_));
gtk_grab_add(window_);
GrabPointerAndKeyboard();
registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED,
NotificationService::AllSources());
theme_provider_->InitThemesFor(this);
}
// NOTE: This seems a bit overcomplicated, but it requires a bunch of careful
// fudging to get the pixels rasterized exactly where we want them, the arrow to
// have a 1 pixel point, etc.
// TODO(deanm): Windows draws with Skia and uses some PNG images for the
// corners. This is a lot more work, but they get anti-aliasing.
// static
std::vector<GdkPoint> InfoBubbleGtk::MakeFramePolygonPoints(
ArrowLocationGtk arrow_location,
int width,
int height,
FrameType type) {
using gtk_util::MakeBidiGdkPoint;
std::vector<GdkPoint> points;
bool on_left = (arrow_location == ARROW_LOCATION_TOP_LEFT);
// If we're stroking the frame, we need to offset some of our points by 1
// pixel. We do this when we draw horizontal lines that are on the bottom or
// when we draw vertical lines that are closer to the end (where "end" is the
// right side for ARROW_LOCATION_TOP_LEFT).
int y_off = (type == FRAME_MASK) ? 0 : -1;
// We use this one for arrows located on the left.
int x_off_l = on_left ? y_off : 0;
// We use this one for RTL.
int x_off_r = !on_left ? -y_off : 0;
// Top left corner.
points.push_back(MakeBidiGdkPoint(
x_off_r, kArrowSize + kCornerSize - 1, width, on_left));
points.push_back(MakeBidiGdkPoint(
kCornerSize + x_off_r - 1, kArrowSize, width, on_left));
// The arrow.
points.push_back(MakeBidiGdkPoint(
kArrowX - kArrowSize + x_off_r, kArrowSize, width, on_left));
points.push_back(MakeBidiGdkPoint(
kArrowX + x_off_r, 0, width, on_left));
points.push_back(MakeBidiGdkPoint(
kArrowX + 1 + x_off_l, 0, width, on_left));
points.push_back(MakeBidiGdkPoint(
kArrowX + kArrowSize + 1 + x_off_l, kArrowSize, width, on_left));
// Top right corner.
points.push_back(MakeBidiGdkPoint(
width - kCornerSize + 1 + x_off_l, kArrowSize, width, on_left));
points.push_back(MakeBidiGdkPoint(
width + x_off_l, kArrowSize + kCornerSize - 1, width, on_left));
// Bottom right corner.
points.push_back(MakeBidiGdkPoint(
width + x_off_l, height - kCornerSize, width, on_left));
points.push_back(MakeBidiGdkPoint(
width - kCornerSize + x_off_r, height + y_off, width, on_left));
// Bottom left corner.
points.push_back(MakeBidiGdkPoint(
kCornerSize + x_off_l, height + y_off, width, on_left));
points.push_back(MakeBidiGdkPoint(
x_off_r, height - kCornerSize, width, on_left));
return points;
}
InfoBubbleGtk::ArrowLocationGtk InfoBubbleGtk::GetArrowLocation(
ArrowLocationGtk preferred_location, int arrow_x, int width) {
bool wants_left = (preferred_location == ARROW_LOCATION_TOP_LEFT);
int screen_width = gdk_screen_get_width(gdk_screen_get_default());
bool left_is_onscreen = (arrow_x - kArrowX + width < screen_width);
bool right_is_onscreen = (arrow_x + kArrowX - width >= 0);
// Use the requested location if it fits onscreen, use whatever fits
// otherwise, and use the requested location if neither fits.
if (left_is_onscreen && (wants_left || !right_is_onscreen))
return ARROW_LOCATION_TOP_LEFT;
if (right_is_onscreen && (!wants_left || !left_is_onscreen))
return ARROW_LOCATION_TOP_RIGHT;
return (wants_left ? ARROW_LOCATION_TOP_LEFT : ARROW_LOCATION_TOP_RIGHT);
}
bool InfoBubbleGtk::UpdateArrowLocation(bool force_move_and_reshape) {
gint toplevel_x = 0, toplevel_y = 0;
gdk_window_get_position(
GTK_WIDGET(toplevel_window_)->window, &toplevel_x, &toplevel_y);
ArrowLocationGtk old_location = current_arrow_location_;
current_arrow_location_ = GetArrowLocation(
preferred_arrow_location_,
toplevel_x + rect_.x() + (rect_.width() / 2), // arrow_x
window_->allocation.width);
if (force_move_and_reshape || current_arrow_location_ != old_location) {
UpdateWindowShape();
MoveWindow();
// We need to redraw the entire window to repaint its border.
gtk_widget_queue_draw(window_);
return true;
}
return false;
}
void InfoBubbleGtk::UpdateWindowShape() {
if (mask_region_) {
gdk_region_destroy(mask_region_);
mask_region_ = NULL;
}
std::vector<GdkPoint> points = MakeFramePolygonPoints(
current_arrow_location_,
window_->allocation.width, window_->allocation.height,
FRAME_MASK);
mask_region_ = gdk_region_polygon(&points[0],
points.size(),
GDK_EVEN_ODD_RULE);
gdk_window_shape_combine_region(window_->window, mask_region_, 0, 0);
}
void InfoBubbleGtk::MoveWindow() {
gint toplevel_x = 0, toplevel_y = 0;
gdk_window_get_position(
GTK_WIDGET(toplevel_window_)->window, &toplevel_x, &toplevel_y);
gint screen_x = 0;
if (current_arrow_location_ == ARROW_LOCATION_TOP_LEFT) {
screen_x = toplevel_x + rect_.x() + (rect_.width() / 2) - kArrowX;
} else if (current_arrow_location_ == ARROW_LOCATION_TOP_RIGHT) {
screen_x = toplevel_x + rect_.x() + (rect_.width() / 2) -
window_->allocation.width + kArrowX;
} else {
NOTREACHED();
}
gint screen_y = toplevel_y + rect_.y() + rect_.height() +
kArrowToContentPadding;
gtk_window_move(GTK_WINDOW(window_), screen_x, screen_y);
}
void InfoBubbleGtk::StackWindow() {
// Stack our window directly above the toplevel window.
gtk_util::StackPopupWindow(window_, GTK_WIDGET(toplevel_window_));
}
void InfoBubbleGtk::Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
DCHECK_EQ(type.value, NotificationType::BROWSER_THEME_CHANGED);
if (theme_provider_->UseGtkTheme() && match_system_theme_) {
gtk_widget_modify_bg(window_, GTK_STATE_NORMAL, NULL);
} else {
// Set the background color, so we don't need to paint it manually.
gtk_widget_modify_bg(window_, GTK_STATE_NORMAL, &kBackgroundColor);
}
}
void InfoBubbleGtk::HandlePointerAndKeyboardUngrabbedByContent() {
GrabPointerAndKeyboard();
}
void InfoBubbleGtk::CloseInternal(bool closed_by_escape) {
// Notify the delegate that we're about to close. This gives the chance
// to save state / etc from the hosted widget before it's destroyed.
if (delegate_)
delegate_->InfoBubbleClosing(this, closed_by_escape);
// We don't need to ungrab the pointer or keyboard here; the X server will
// automatically do that when we destroy our window.
DCHECK(window_);
gtk_widget_destroy(window_);
// |this| has been deleted, see HandleDestroy.
}
void InfoBubbleGtk::GrabPointerAndKeyboard() {
// Install X pointer and keyboard grabs to make sure that we have the focus
// and get all mouse and keyboard events until we're closed.
GdkGrabStatus pointer_grab_status =
gdk_pointer_grab(window_->window,
TRUE, // owner_events
GDK_BUTTON_PRESS_MASK, // event_mask
NULL, // confine_to
NULL, // cursor
GDK_CURRENT_TIME);
if (pointer_grab_status != GDK_GRAB_SUCCESS) {
// This will fail if someone else already has the pointer grabbed, but
// there's not really anything we can do about that.
DLOG(ERROR) << "Unable to grab pointer (status="
<< pointer_grab_status << ")";
}
GdkGrabStatus keyboard_grab_status =
gdk_keyboard_grab(window_->window,
FALSE, // owner_events
GDK_CURRENT_TIME);
if (keyboard_grab_status != GDK_GRAB_SUCCESS) {
DLOG(ERROR) << "Unable to grab keyboard (status="
<< keyboard_grab_status << ")";
}
}
gboolean InfoBubbleGtk::HandleEscape() {
CloseInternal(true); // Close by escape.
return TRUE;
}
gboolean InfoBubbleGtk::HandleExpose() {
GdkDrawable* drawable = GDK_DRAWABLE(window_->window);
GdkGC* gc = gdk_gc_new(drawable);
gdk_gc_set_rgb_fg_color(gc, &kFrameColor);
// Stroke the frame border.
std::vector<GdkPoint> points = MakeFramePolygonPoints(
current_arrow_location_,
window_->allocation.width, window_->allocation.height,
FRAME_STROKE);
gdk_draw_polygon(drawable, gc, FALSE, &points[0], points.size());
g_object_unref(gc);
return FALSE; // Propagate so our children paint, etc.
}
// When our size is initially allocated or changed, we need to recompute
// and apply our shape mask region.
void InfoBubbleGtk::HandleSizeAllocate() {
if (!UpdateArrowLocation(false)) {
UpdateWindowShape();
if (current_arrow_location_ == ARROW_LOCATION_TOP_RIGHT)
MoveWindow();
}
}
gboolean InfoBubbleGtk::HandleButtonPress(GdkEventButton* event) {
// If we got a click in our own window, that's okay (we need to additionally
// check that it falls within our bounds, since we've grabbed the pointer and
// some events that actually occurred in other windows will be reported with
// respect to our window).
if (event->window == window_->window &&
(mask_region_ && gdk_region_point_in(mask_region_, event->x, event->y))) {
return FALSE; // Propagate.
}
// Our content widget got a click.
if (event->window != window_->window &&
gdk_window_get_toplevel(event->window) == window_->window) {
return FALSE;
}
// Otherwise we had a click outside of our window, close ourself.
Close();
return TRUE;
}
gboolean InfoBubbleGtk::HandleDestroy() {
// We are self deleting, we have a destroy signal setup to catch when we
// destroy the widget manually, or the window was closed via X. This will
// delete the InfoBubbleGtk object.
delete this;
return FALSE; // Propagate.
}
gboolean InfoBubbleGtk::HandleToplevelConfigure(GdkEventConfigure* event) {
if (!UpdateArrowLocation(false))
MoveWindow();
StackWindow();
return FALSE;
}
gboolean InfoBubbleGtk::HandleToplevelUnmap() {
Close();
return FALSE;
}