blob: f6d1ccdfcb5a72148c9ad1884537059c378693dc [file] [log] [blame]
// Copyright (c) 2012 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/ui/gtk/gtk_util.h"
#include <cairo/cairo.h>
#include <algorithm>
#include <cstdarg>
#include <map>
#include "base/environment.h"
#include "base/i18n/rtl.h"
#include "base/logging.h"
#include "base/nix/xdg_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/autocomplete/autocomplete_classifier.h"
#include "chrome/browser/autocomplete/autocomplete_classifier_factory.h"
#include "chrome/browser/autocomplete/autocomplete_match.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_info_cache.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_iterator.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/gtk/browser_window_gtk.h"
#include "chrome/browser/ui/gtk/gtk_theme_service.h"
#include "chrome/browser/ui/host_desktop.h"
#include "grit/chrome_unscaled_resources.h"
#include "grit/theme_resources.h"
#include "ui/base/accelerators/menu_label_accelerator_util_linux.h"
#include "ui/base/gtk/gtk_hig_constants.h"
#include "ui/base/gtk/gtk_screen_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/x/x11_util.h"
#include "ui/gfx/gtk_compat.h"
#include "ui/gfx/image/cairo_cached_surface.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/pango_util.h"
#include "ui/gfx/text_elider.h"
#include "url/gurl.h"
// These conflict with base/tracked_objects.h, so need to come last.
#include <gdk/gdkx.h> // NOLINT
namespace {
#if defined(GOOGLE_CHROME_BUILD)
static const char* kIconName = "google-chrome";
#else
static const char* kIconName = "chromium-browser";
#endif
const char kBoldLabelMarkup[] = "<span weight='bold'>%s</span>";
// Max size of each component of the button tooltips.
const size_t kMaxTooltipTitleLength = 100;
const size_t kMaxTooltipURLLength = 400;
// Callback used in RemoveAllChildren.
void RemoveWidget(GtkWidget* widget, gpointer container) {
gtk_container_remove(GTK_CONTAINER(container), widget);
}
// These two functions are copped almost directly from gtk core. The only
// difference is that they accept middle clicks.
gboolean OnMouseButtonPressed(GtkWidget* widget, GdkEventButton* event,
gpointer userdata) {
if (event->type == GDK_BUTTON_PRESS) {
if (gtk_button_get_focus_on_click(GTK_BUTTON(widget)) &&
!gtk_widget_has_focus(widget)) {
gtk_widget_grab_focus(widget);
}
gint button_mask = GPOINTER_TO_INT(userdata);
if (button_mask & (1 << event->button))
gtk_button_pressed(GTK_BUTTON(widget));
}
return TRUE;
}
gboolean OnMouseButtonReleased(GtkWidget* widget, GdkEventButton* event,
gpointer userdata) {
gint button_mask = GPOINTER_TO_INT(userdata);
if (button_mask && (1 << event->button))
gtk_button_released(GTK_BUTTON(widget));
return TRUE;
}
// Returns the approximate number of characters that can horizontally fit in
// |pixel_width| pixels.
int GetCharacterWidthForPixels(GtkWidget* widget, int pixel_width) {
DCHECK(gtk_widget_get_realized(widget))
<< " widget must be realized to compute font metrics correctly";
PangoContext* context = gtk_widget_create_pango_context(widget);
GtkStyle* style = gtk_widget_get_style(widget);
PangoFontMetrics* metrics = pango_context_get_metrics(context,
style->font_desc, pango_context_get_language(context));
// This technique (max of char and digit widths) matches the code in
// gtklabel.c.
int char_width = pixel_width * PANGO_SCALE /
std::max(pango_font_metrics_get_approximate_char_width(metrics),
pango_font_metrics_get_approximate_digit_width(metrics));
pango_font_metrics_unref(metrics);
g_object_unref(context);
return char_width;
}
void OnLabelRealize(GtkWidget* label, gpointer pixel_width) {
gtk_label_set_width_chars(
GTK_LABEL(label),
GetCharacterWidthForPixels(label, GPOINTER_TO_INT(pixel_width)));
}
// Ownership of |icon_list| is passed to the caller.
GList* GetIconList() {
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
GList* icon_list = NULL;
icon_list = g_list_append(icon_list,
rb.GetNativeImageNamed(IDR_PRODUCT_LOGO_32).ToGdkPixbuf());
icon_list = g_list_append(icon_list,
rb.GetNativeImageNamed(IDR_PRODUCT_LOGO_16).ToGdkPixbuf());
return icon_list;
}
// Returns the avatar icon for |profile|.
//
// Returns NULL if there is only one profile; always returns an icon for
// Incognito profiles.
//
// The returned pixbuf must not be unreferenced or freed because it's owned by
// either the resource bundle or the profile info cache.
GdkPixbuf* GetAvatarIcon(Profile* profile) {
if (profile->IsOffTheRecord()) {
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
return rb.GetNativeImageNamed(IDR_OTR_ICON).ToGdkPixbuf();
}
const ProfileInfoCache& cache =
g_browser_process->profile_manager()->GetProfileInfoCache();
if (!profiles::IsMultipleProfilesEnabled() ||
cache.GetNumberOfProfiles() < 2)
return NULL;
const size_t index = cache.GetIndexOfProfileWithPath(profile->GetPath());
return (index != std::string::npos ?
cache.GetAvatarIconOfProfileAtIndex(index).ToGdkPixbuf() :
static_cast<GdkPixbuf*>(NULL));
}
// Gets the Chrome product icon.
//
// If it doesn't find the icon in |theme|, it looks among the icons packaged
// with Chrome.
//
// Supported values of |size| are 16, 32, and 64. If the Chrome icon is found
// in |theme|, the returned icon may not be of the requested size if |size|
// has an unsupported value (GTK might scale it). If the Chrome icon is not
// found in |theme|, and |size| has an unsupported value, the program will be
// aborted with CHECK(false).
//
// The caller is responsible for calling g_object_unref() on the returned
// pixbuf.
GdkPixbuf* GetChromeIcon(GtkIconTheme* theme, const int size) {
if (gtk_icon_theme_has_icon(theme, kIconName)) {
GdkPixbuf* icon =
gtk_icon_theme_load_icon(theme,
kIconName,
size,
static_cast<GtkIconLookupFlags>(0),
0);
GdkPixbuf* icon_copy = gdk_pixbuf_copy(icon);
g_object_unref(icon);
return icon_copy;
}
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
int id = 0;
switch (size) {
case 16: id = IDR_PRODUCT_LOGO_16; break;
case 32: id = IDR_PRODUCT_LOGO_32; break;
case 64: id = IDR_PRODUCT_LOGO_64; break;
default: CHECK(false); break;
}
return gdk_pixbuf_copy(rb.GetNativeImageNamed(id).ToGdkPixbuf());
}
// Adds |emblem| to the bottom-right corner of |icon|.
//
// Taking the ceiling of the scaled destination rect's dimensions (|dest_w|
// and |dest_h|) because, if the destination rect is larger than the scaled
// emblem, gdk_pixbuf_composite() will replicate the edge pixels of the emblem
// to fill the gap, which is better than a cropped emblem, I think.
void AddEmblem(const GdkPixbuf* emblem, GdkPixbuf* icon) {
const int iw = gdk_pixbuf_get_width(icon);
const int ih = gdk_pixbuf_get_height(icon);
const int ew = gdk_pixbuf_get_width(emblem);
const int eh = gdk_pixbuf_get_height(emblem);
const double emblem_scale =
(static_cast<double>(ih) / static_cast<double>(eh)) * 0.5;
const int dest_w = ::ceil(ew * emblem_scale);
const int dest_h = ::ceil(eh * emblem_scale);
const int x = iw - dest_w; // Used for offset_x and dest_x.
const int y = ih - dest_h; // Used for offset_y and dest_y.
gdk_pixbuf_composite(emblem, icon,
x, y,
dest_w, dest_h,
x, y,
emblem_scale, emblem_scale,
GDK_INTERP_BILINEAR, 255);
}
// Returns a list containing Chrome icons of various sizes emblemed with the
// |profile|'s avatar.
//
// If there is only one profile, no emblem is added, but icons for Incognito
// profiles will always get the Incognito emblem.
//
// The caller owns the list and all the icons it contains will have had their
// reference counts incremented. Therefore the caller should unreference each
// element before freeing the list.
GList* GetIconListWithAvatars(GtkWindow* window, Profile* profile) {
GtkIconTheme* theme =
gtk_icon_theme_get_for_screen(gtk_widget_get_screen(GTK_WIDGET(window)));
GdkPixbuf* icon_16 = GetChromeIcon(theme, 16);
GdkPixbuf* icon_32 = GetChromeIcon(theme, 32);
GdkPixbuf* icon_64 = GetChromeIcon(theme, 64);
const GdkPixbuf* avatar = GetAvatarIcon(profile);
if (avatar) {
AddEmblem(avatar, icon_16);
AddEmblem(avatar, icon_32);
AddEmblem(avatar, icon_64);
}
GList* icon_list = NULL;
icon_list = g_list_append(icon_list, icon_64);
icon_list = g_list_append(icon_list, icon_32);
icon_list = g_list_append(icon_list, icon_16);
return icon_list;
}
// Expose event handler for a container that simply suppresses the default
// drawing and propagates the expose event to the container's children.
gboolean PaintNoBackground(GtkWidget* widget,
GdkEventExpose* event,
gpointer unused) {
GList* children = gtk_container_get_children(GTK_CONTAINER(widget));
for (GList* item = children; item; item = item->next) {
gtk_container_propagate_expose(GTK_CONTAINER(widget),
GTK_WIDGET(item->data),
event);
}
g_list_free(children);
return TRUE;
}
} // namespace
namespace gtk_util {
GtkWidget* CreateLabeledControlsGroup(std::vector<GtkWidget*>* labels,
const char* text, ...) {
va_list ap;
va_start(ap, text);
GtkWidget* table = gtk_table_new(0, 2, FALSE);
gtk_table_set_col_spacing(GTK_TABLE(table), 0, ui::kLabelSpacing);
gtk_table_set_row_spacings(GTK_TABLE(table), ui::kControlSpacing);
for (guint row = 0; text; ++row) {
gtk_table_resize(GTK_TABLE(table), row + 1, 2);
GtkWidget* control = va_arg(ap, GtkWidget*);
GtkWidget* label = gtk_label_new(text);
gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
if (labels)
labels->push_back(label);
gtk_table_attach(GTK_TABLE(table), label,
0, 1, row, row + 1,
GTK_FILL, GTK_FILL,
0, 0);
gtk_table_attach_defaults(GTK_TABLE(table), control,
1, 2, row, row + 1);
text = va_arg(ap, const char*);
}
va_end(ap);
return table;
}
GtkWidget* CreateGtkBorderBin(GtkWidget* child, const GdkColor* color,
int top, int bottom, int left, int right) {
// Use a GtkEventBox to get the background painted. However, we can't just
// use a container border, since it won't paint there. Use an alignment
// inside to get the sizes exactly of how we want the border painted.
GtkWidget* ebox = gtk_event_box_new();
if (color)
gtk_widget_modify_bg(ebox, GTK_STATE_NORMAL, color);
GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
gtk_alignment_set_padding(GTK_ALIGNMENT(alignment), top, bottom, left, right);
gtk_container_add(GTK_CONTAINER(alignment), child);
gtk_container_add(GTK_CONTAINER(ebox), alignment);
return ebox;
}
GtkWidget* LeftAlignMisc(GtkWidget* misc) {
gtk_misc_set_alignment(GTK_MISC(misc), 0, 0.5);
return misc;
}
GtkWidget* CreateBoldLabel(const std::string& text) {
GtkWidget* label = gtk_label_new(NULL);
char* markup = g_markup_printf_escaped(kBoldLabelMarkup, text.c_str());
gtk_label_set_markup(GTK_LABEL(label), markup);
g_free(markup);
return LeftAlignMisc(label);
}
void GetWidgetSizeFromCharacters(
GtkWidget* widget, double width_chars, double height_lines,
int* width, int* height) {
DCHECK(gtk_widget_get_realized(widget))
<< " widget must be realized to compute font metrics correctly";
PangoContext* context = gtk_widget_create_pango_context(widget);
GtkStyle* style = gtk_widget_get_style(widget);
PangoFontMetrics* metrics = pango_context_get_metrics(context,
style->font_desc, pango_context_get_language(context));
if (width) {
*width = static_cast<int>(
pango_font_metrics_get_approximate_char_width(metrics) *
width_chars / PANGO_SCALE);
}
if (height) {
*height = static_cast<int>(
(pango_font_metrics_get_ascent(metrics) +
pango_font_metrics_get_descent(metrics)) *
height_lines / PANGO_SCALE);
}
pango_font_metrics_unref(metrics);
g_object_unref(context);
}
void GetWidgetSizeFromResources(
GtkWidget* widget, int width_chars, int height_lines,
int* width, int* height) {
DCHECK(gtk_widget_get_realized(widget))
<< " widget must be realized to compute font metrics correctly";
double chars = 0;
if (width)
base::StringToDouble(l10n_util::GetStringUTF8(width_chars), &chars);
double lines = 0;
if (height)
base::StringToDouble(l10n_util::GetStringUTF8(height_lines), &lines);
GetWidgetSizeFromCharacters(widget, chars, lines, width, height);
}
void SetWindowSizeFromResources(GtkWindow* window,
int width_id, int height_id, bool resizable) {
int width = -1;
int height = -1;
gtk_util::GetWidgetSizeFromResources(GTK_WIDGET(window), width_id, height_id,
(width_id != -1) ? &width : NULL,
(height_id != -1) ? &height : NULL);
if (resizable) {
gtk_window_set_default_size(window, width, height);
} else {
// For a non-resizable window, GTK tries to snap the window size
// to the minimum size around the content. We use the sizes in
// the resources to set *minimum* window size to allow windows
// with long titles to be wide enough to display their titles.
//
// But if GTK wants to make the window *wider* due to very wide
// controls, we should allow that too, so be careful to pick the
// wider of the resources size and the natural window size.
gtk_widget_show_all(gtk_bin_get_child(GTK_BIN(window)));
GtkRequisition requisition;
gtk_widget_size_request(GTK_WIDGET(window), &requisition);
gtk_widget_set_size_request(
GTK_WIDGET(window),
width == -1 ? -1 : std::max(width, requisition.width),
height == -1 ? -1 : std::max(height, requisition.height));
}
gtk_window_set_resizable(window, resizable ? TRUE : FALSE);
}
void MakeAppModalWindowGroup() {
// Older versions of GTK+ don't give us gtk_window_group_list() which is what
// we need to add current non-browser modal dialogs to the list. If
// we have 2.14+ we can do things the correct way.
GtkWindowGroup* window_group = gtk_window_group_new();
for (chrome::BrowserIterator it; !it.done(); it.Next()) {
// List all windows in this current group
GtkWindowGroup* old_group =
gtk_window_get_group((*it)->window()->GetNativeWindow());
GList* all_windows = gtk_window_group_list_windows(old_group);
for (GList* window = all_windows; window; window = window->next) {
gtk_window_group_add_window(window_group, GTK_WINDOW(window->data));
}
g_list_free(all_windows);
}
g_object_unref(window_group);
}
void AppModalDismissedUngroupWindows() {
// GTK only has the native desktop.
const BrowserList* native_browser_list =
BrowserList::GetInstance(chrome::HOST_DESKTOP_TYPE_NATIVE);
if (!native_browser_list->empty()) {
std::vector<GtkWindow*> transient_windows;
// All windows should be part of one big modal group right now.
GtkWindowGroup* window_group = gtk_window_get_group(
native_browser_list->get(0)->window()->GetNativeWindow());
GList* windows = gtk_window_group_list_windows(window_group);
for (GList* item = windows; item; item = item->next) {
GtkWindow* window = GTK_WINDOW(item->data);
GtkWindow* transient_for = gtk_window_get_transient_for(window);
if (transient_for) {
transient_windows.push_back(window);
} else {
GtkWindowGroup* window_group = gtk_window_group_new();
gtk_window_group_add_window(window_group, window);
g_object_unref(window_group);
}
}
// Put each transient window in the same group as its transient parent.
for (std::vector<GtkWindow*>::iterator it = transient_windows.begin();
it != transient_windows.end(); ++it) {
GtkWindow* transient_parent = gtk_window_get_transient_for(*it);
GtkWindowGroup* group = gtk_window_get_group(transient_parent);
gtk_window_group_add_window(group, *it);
}
g_list_free(windows);
}
}
void RemoveAllChildren(GtkWidget* container) {
gtk_container_foreach(GTK_CONTAINER(container), RemoveWidget, container);
}
void ForceFontSizePixels(GtkWidget* widget, double size_pixels) {
gfx::ScopedPangoFontDescription font_desc(pango_font_description_new());
// pango_font_description_set_absolute_size sets the font size in device
// units, which for us is pixels.
pango_font_description_set_absolute_size(font_desc.get(),
PANGO_SCALE * size_pixels);
gtk_widget_modify_font(widget, font_desc.get());
}
void UndoForceFontSize(GtkWidget* widget) {
gtk_widget_modify_font(widget, NULL);
}
gfx::Size GetWidgetSize(GtkWidget* widget) {
GtkRequisition size;
gtk_widget_size_request(widget, &size);
return gfx::Size(size.width, size.height);
}
void ConvertWidgetPointToScreen(GtkWidget* widget, gfx::Point* p) {
DCHECK(widget);
DCHECK(p);
*p += ui::GetWidgetScreenOffset(widget);
}
GtkWidget* CenterWidgetInHBox(GtkWidget* hbox, GtkWidget* widget,
bool pack_at_end, int padding) {
GtkWidget* centering_vbox = gtk_vbox_new(FALSE, 0);
gtk_box_pack_start(GTK_BOX(centering_vbox), widget, TRUE, FALSE, 0);
if (pack_at_end)
gtk_box_pack_end(GTK_BOX(hbox), centering_vbox, FALSE, FALSE, padding);
else
gtk_box_pack_start(GTK_BOX(hbox), centering_vbox, FALSE, FALSE, padding);
return centering_vbox;
}
void SetButtonClickableByMouseButtons(GtkWidget* button,
bool left, bool middle, bool right) {
gint button_mask = 0;
if (left)
button_mask |= 1 << 1;
if (middle)
button_mask |= 1 << 2;
if (right)
button_mask |= 1 << 3;
void* userdata = GINT_TO_POINTER(button_mask);
g_signal_connect(button, "button-press-event",
G_CALLBACK(OnMouseButtonPressed), userdata);
g_signal_connect(button, "button-release-event",
G_CALLBACK(OnMouseButtonReleased), userdata);
}
void SetButtonTriggersNavigation(GtkWidget* button) {
SetButtonClickableByMouseButtons(button, true, true, false);
}
int MirroredLeftPointForRect(GtkWidget* widget, const gfx::Rect& bounds) {
if (!base::i18n::IsRTL())
return bounds.x();
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
return allocation.width - bounds.x() - bounds.width();
}
int MirroredRightPointForRect(GtkWidget* widget, const gfx::Rect& bounds) {
if (!base::i18n::IsRTL())
return bounds.right();
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
return allocation.width - bounds.x();
}
int MirroredXCoordinate(GtkWidget* widget, int x) {
if (base::i18n::IsRTL()) {
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
return allocation.width - x;
}
return x;
}
bool WidgetContainsCursor(GtkWidget* widget) {
gint x = 0;
gint y = 0;
gtk_widget_get_pointer(widget, &x, &y);
return WidgetBounds(widget).Contains(x, y);
}
void SetDefaultWindowIcon(GtkWindow* window) {
GtkIconTheme* theme =
gtk_icon_theme_get_for_screen(gtk_widget_get_screen(GTK_WIDGET(window)));
if (gtk_icon_theme_has_icon(theme, kIconName)) {
gtk_window_set_default_icon_name(kIconName);
// Sometimes the WM fails to update the icon when we tell it to. The above
// line should be enough to update all existing windows, but it can fail,
// e.g. with Lucid/metacity. The following line seems to fix the common
// case where the first window created doesn't have an icon.
gtk_window_set_icon_name(window, kIconName);
} else {
GList* icon_list = GetIconList();
gtk_window_set_default_icon_list(icon_list);
// Same logic applies here.
gtk_window_set_icon_list(window, icon_list);
g_list_free(icon_list);
}
}
void SetWindowIcon(GtkWindow* window, Profile* profile) {
GList* icon_list = GetIconListWithAvatars(window, profile);
gtk_window_set_icon_list(window, icon_list);
g_list_foreach(icon_list, reinterpret_cast<GFunc>(g_object_unref), NULL);
g_list_free(icon_list);
}
void SetWindowIcon(GtkWindow* window, Profile* profile, GdkPixbuf* icon) {
const GdkPixbuf* avatar = GetAvatarIcon(profile);
if (avatar) AddEmblem(avatar, icon);
gtk_window_set_icon(window, icon);
}
GtkWidget* AddButtonToDialog(GtkWidget* dialog, const gchar* text,
const gchar* stock_id, gint response_id) {
GtkWidget* button = gtk_button_new_with_label(text);
gtk_button_set_image(GTK_BUTTON(button),
gtk_image_new_from_stock(stock_id,
GTK_ICON_SIZE_BUTTON));
gtk_dialog_add_action_widget(GTK_DIALOG(dialog), button,
response_id);
return button;
}
GtkWidget* BuildDialogButton(GtkWidget* dialog, int ids_id,
const gchar* stock_id) {
GtkWidget* button = gtk_button_new_with_mnemonic(
ui::ConvertAcceleratorsFromWindowsStyle(
l10n_util::GetStringUTF8(ids_id)).c_str());
gtk_button_set_image(GTK_BUTTON(button),
gtk_image_new_from_stock(stock_id,
GTK_ICON_SIZE_BUTTON));
return button;
}
GtkWidget* CreateEntryImageHBox(GtkWidget* entry, GtkWidget* image) {
GtkWidget* hbox = gtk_hbox_new(FALSE, ui::kControlSpacing);
gtk_box_pack_start(GTK_BOX(hbox), entry, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0);
return hbox;
}
void SetLabelColor(GtkWidget* label, const GdkColor* color) {
gtk_widget_modify_fg(label, GTK_STATE_NORMAL, color);
gtk_widget_modify_fg(label, GTK_STATE_ACTIVE, color);
gtk_widget_modify_fg(label, GTK_STATE_PRELIGHT, color);
gtk_widget_modify_fg(label, GTK_STATE_INSENSITIVE, color);
}
GtkWidget* IndentWidget(GtkWidget* content) {
GtkWidget* content_alignment = gtk_alignment_new(0.0, 0.5, 1.0, 1.0);
gtk_alignment_set_padding(GTK_ALIGNMENT(content_alignment), 0, 0,
ui::kGroupIndent, 0);
gtk_container_add(GTK_CONTAINER(content_alignment), content);
return content_alignment;
}
GdkPoint MakeBidiGdkPoint(gint x, gint y, gint width, bool ltr) {
GdkPoint point = {ltr ? x : width - x, y};
return point;
}
std::string BuildTooltipTitleFor(base::string16 title, const GURL& url) {
const std::string& url_str = url.possibly_invalid_spec();
const std::string& title_str = UTF16ToUTF8(title);
std::string truncated_url = UTF16ToUTF8(gfx::TruncateString(
UTF8ToUTF16(url_str), kMaxTooltipURLLength));
gchar* escaped_url_cstr = g_markup_escape_text(truncated_url.c_str(),
truncated_url.size());
std::string escaped_url(escaped_url_cstr);
g_free(escaped_url_cstr);
if (url_str == title_str || title.empty()) {
return escaped_url;
} else {
std::string truncated_title = UTF16ToUTF8(gfx::TruncateString(
title, kMaxTooltipTitleLength));
gchar* escaped_title_cstr = g_markup_escape_text(truncated_title.c_str(),
truncated_title.size());
std::string escaped_title(escaped_title_cstr);
g_free(escaped_title_cstr);
if (!escaped_url.empty())
return std::string("<b>") + escaped_title + "</b>\n" + escaped_url;
else
return std::string("<b>") + escaped_title + "</b>";
}
}
void DrawTextEntryBackground(GtkWidget* offscreen_entry,
GtkWidget* widget_to_draw_on,
GdkRectangle* dirty_rec,
GdkRectangle* rec) {
GtkStyle* gtk_owned_style = gtk_rc_get_style(offscreen_entry);
// GTK owns the above and we're going to have to make our own copy of it
// that we can edit.
GtkStyle* our_style = gtk_style_copy(gtk_owned_style);
our_style = gtk_style_attach(our_style, widget_to_draw_on->window);
// TODO(erg): Draw the focus ring if appropriate...
// We're using GTK rendering; draw a GTK entry widget onto the background.
gtk_paint_shadow(our_style, widget_to_draw_on->window,
GTK_STATE_NORMAL, GTK_SHADOW_IN, dirty_rec,
widget_to_draw_on, "entry",
rec->x, rec->y, rec->width, rec->height);
// Draw the interior background (not all themes draw the entry background
// above; this is a noop on themes that do).
gint xborder = our_style->xthickness;
gint yborder = our_style->ythickness;
gint width = rec->width - 2 * xborder;
gint height = rec->height - 2 * yborder;
if (width > 0 && height > 0) {
gtk_paint_flat_box(our_style, widget_to_draw_on->window,
GTK_STATE_NORMAL, GTK_SHADOW_NONE, dirty_rec,
widget_to_draw_on, "entry_bg",
rec->x + xborder, rec->y + yborder,
width, height);
}
gtk_style_detach(our_style);
g_object_unref(our_style);
}
void SetLayoutText(PangoLayout* layout, const base::string16& text) {
// Pango is really easy to overflow and send into a computational death
// spiral that can corrupt the screen. Assume that we'll never have more than
// 2000 characters, which should be a safe assumption until we all get robot
// eyes. http://crbug.com/66576
std::string text_utf8 = UTF16ToUTF8(text);
if (text_utf8.length() > 2000)
text_utf8 = text_utf8.substr(0, 2000);
pango_layout_set_text(layout, text_utf8.data(), text_utf8.length());
}
void DrawThemedToolbarBackground(GtkWidget* widget,
cairo_t* cr,
GdkEventExpose* event,
const gfx::Point& tabstrip_origin,
GtkThemeService* theme_service) {
// Fill the entire region with the toolbar color.
GdkColor color = theme_service->GetGdkColor(
ThemeProperties::COLOR_TOOLBAR);
gdk_cairo_set_source_color(cr, &color);
cairo_fill(cr);
// The toolbar is supposed to blend in with the active tab, so we have to pass
// coordinates for the IDR_THEME_TOOLBAR bitmap relative to the top of the
// tab strip.
const gfx::Image background =
theme_service->GetImageNamed(IDR_THEME_TOOLBAR);
background.ToCairo()->SetSource(cr, widget,
tabstrip_origin.x(), tabstrip_origin.y());
// We tile the toolbar background in both directions.
cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT);
cairo_rectangle(cr,
tabstrip_origin.x(),
tabstrip_origin.y(),
event->area.x + event->area.width - tabstrip_origin.x(),
event->area.y + event->area.height - tabstrip_origin.y());
cairo_fill(cr);
}
void DrawFullImage(cairo_t* cr,
GtkWidget* widget,
const gfx::Image& image,
gint dest_x,
gint dest_y) {
gfx::CairoCachedSurface* surface = image.ToCairo();
surface->SetSource(cr, widget, dest_x, dest_y);
cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT);
cairo_rectangle(cr, dest_x, dest_y, surface->Width(), surface->Height());
cairo_fill(cr);
}
GdkColor AverageColors(GdkColor color_one, GdkColor color_two) {
GdkColor average_color;
average_color.pixel = 0;
average_color.red = (color_one.red + color_two.red) / 2;
average_color.green = (color_one.green + color_two.green) / 2;
average_color.blue = (color_one.blue + color_two.blue) / 2;
return average_color;
}
void SetAlwaysShowImage(GtkWidget* image_menu_item) {
gtk_image_menu_item_set_always_show_image(
GTK_IMAGE_MENU_ITEM(image_menu_item), TRUE);
}
gfx::Rect GetWidgetRectRelativeToToplevel(GtkWidget* widget) {
DCHECK(gtk_widget_get_realized(widget));
GtkWidget* toplevel = gtk_widget_get_toplevel(widget);
DCHECK(toplevel);
DCHECK(gtk_widget_get_realized(toplevel));
gint x = 0, y = 0;
gtk_widget_translate_coordinates(widget,
toplevel,
0, 0,
&x, &y);
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
return gfx::Rect(x, y, allocation.width, allocation.height);
}
void SuppressDefaultPainting(GtkWidget* container) {
g_signal_connect(container, "expose-event",
G_CALLBACK(PaintNoBackground), NULL);
}
bool GrabAllInput(GtkWidget* widget) {
guint time = gtk_get_current_event_time();
if (!gtk_widget_get_visible(widget))
return false;
GdkWindow* gdk_window = gtk_widget_get_window(widget);
if (gdk_pointer_grab(gdk_window,
TRUE,
GdkEventMask(GDK_BUTTON_PRESS_MASK |
GDK_BUTTON_RELEASE_MASK |
GDK_ENTER_NOTIFY_MASK |
GDK_LEAVE_NOTIFY_MASK |
GDK_POINTER_MOTION_MASK),
NULL, NULL, time) != 0) {
return false;
}
if (gdk_keyboard_grab(gdk_window, TRUE, time) != 0) {
gdk_display_pointer_ungrab(gdk_drawable_get_display(gdk_window), time);
return false;
}
gtk_grab_add(widget);
return true;
}
gfx::Rect WidgetBounds(GtkWidget* widget) {
// To quote the gtk docs:
//
// Widget coordinates are a bit odd; for historical reasons, they are
// defined as widget->window coordinates for widgets that are not
// GTK_NO_WINDOW widgets, and are relative to allocation.x, allocation.y
// for widgets that are GTK_NO_WINDOW widgets.
//
// So the base is always (0,0).
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
return gfx::Rect(0, 0, allocation.width, allocation.height);
}
void SetWMLastUserActionTime(GtkWindow* window) {
gdk_x11_window_set_user_time(gtk_widget_get_window(GTK_WIDGET(window)),
XTimeNow());
}
guint32 XTimeNow() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
bool URLFromPrimarySelection(Profile* profile, GURL* url) {
GtkClipboard* clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
DCHECK(clipboard);
gchar* selection_text = gtk_clipboard_wait_for_text(clipboard);
if (!selection_text)
return false;
// Use autocomplete to clean up the text, going so far as to turn it into
// a search query if necessary.
AutocompleteMatch match;
AutocompleteClassifierFactory::GetForProfile(profile)->Classify(
UTF8ToUTF16(selection_text), false, false, &match, NULL);
g_free(selection_text);
if (!match.destination_url.is_valid())
return false;
*url = match.destination_url;
return true;
}
bool AddWindowAlphaChannel(GtkWidget* window) {
GdkScreen* screen = gtk_widget_get_screen(window);
GdkColormap* rgba = gdk_screen_get_rgba_colormap(screen);
if (rgba)
gtk_widget_set_colormap(window, rgba);
return rgba;
}
void GetTextColors(GdkColor* normal_base,
GdkColor* selected_base,
GdkColor* normal_text,
GdkColor* selected_text) {
GtkWidget* fake_entry = gtk_entry_new();
GtkStyle* style = gtk_rc_get_style(fake_entry);
if (normal_base)
*normal_base = style->base[GTK_STATE_NORMAL];
if (selected_base)
*selected_base = style->base[GTK_STATE_SELECTED];
if (normal_text)
*normal_text = style->text[GTK_STATE_NORMAL];
if (selected_text)
*selected_text = style->text[GTK_STATE_SELECTED];
g_object_ref_sink(fake_entry);
g_object_unref(fake_entry);
}
void ShowDialog(GtkWidget* dialog) {
gtk_widget_show_all(dialog);
}
void ShowDialogWithLocalizedSize(GtkWidget* dialog,
int width_id,
int height_id,
bool resizeable) {
gtk_widget_realize(dialog);
SetWindowSizeFromResources(GTK_WINDOW(dialog),
width_id,
height_id,
resizeable);
gtk_widget_show_all(dialog);
}
void ShowDialogWithMinLocalizedWidth(GtkWidget* dialog,
int width_id) {
gtk_widget_show_all(dialog);
// Suggest a minimum size.
gint width;
GtkRequisition req;
gtk_widget_size_request(dialog, &req);
gtk_util::GetWidgetSizeFromResources(dialog, width_id, 0, &width, NULL);
if (width > req.width)
gtk_widget_set_size_request(dialog, width, -1);
}
void PresentWindow(GtkWidget* window, int timestamp) {
if (timestamp)
gtk_window_present_with_time(GTK_WINDOW(window), timestamp);
else
gtk_window_present(GTK_WINDOW(window));
}
gfx::Rect GetDialogBounds(GtkWidget* dialog) {
gint x = 0, y = 0, width = 1, height = 1;
gtk_window_get_position(GTK_WINDOW(dialog), &x, &y);
gtk_window_get_size(GTK_WINDOW(dialog), &width, &height);
return gfx::Rect(x, y, width, height);
}
base::string16 GetStockPreferencesMenuLabel() {
GtkStockItem stock_item;
base::string16 preferences;
if (gtk_stock_lookup(GTK_STOCK_PREFERENCES, &stock_item)) {
const char16 kUnderscore[] = { '_', 0 };
base::RemoveChars(UTF8ToUTF16(stock_item.label), kUnderscore, &preferences);
}
return preferences;
}
bool IsWidgetAncestryVisible(GtkWidget* widget) {
GtkWidget* parent = widget;
while (parent && gtk_widget_get_visible(parent))
parent = gtk_widget_get_parent(parent);
return !parent;
}
void SetLabelWidth(GtkWidget* label, int pixel_width) {
gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
// Do the simple thing in LTR because the bug only affects right-aligned
// text. Also, when using the workaround, the label tries to maintain
// uniform line-length, which we don't really want.
if (gtk_widget_get_direction(label) == GTK_TEXT_DIR_LTR) {
gtk_widget_set_size_request(label, pixel_width, -1);
} else {
// The label has to be realized before we can adjust its width.
if (gtk_widget_get_realized(label)) {
OnLabelRealize(label, GINT_TO_POINTER(pixel_width));
} else {
g_signal_connect(label, "realize", G_CALLBACK(OnLabelRealize),
GINT_TO_POINTER(pixel_width));
}
}
}
void InitLabelSizeRequestAndEllipsizeMode(GtkWidget* label) {
GtkRequisition size;
gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_NONE);
gtk_widget_set_size_request(label, -1, -1);
gtk_widget_size_request(label, &size);
gtk_widget_set_size_request(label, size.width, size.height);
gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_END);
}
void ApplyMessageDialogQuirks(GtkWidget* dialog) {
if (gtk_window_get_modal(GTK_WINDOW(dialog))) {
// Work around a KDE 3 window manager bug.
scoped_ptr<base::Environment> env(base::Environment::Create());
if (base::nix::DESKTOP_ENVIRONMENT_KDE3 ==
base::nix::GetDesktopEnvironment(env.get()))
gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), FALSE);
}
}
} // namespace gtk_util