1// Copyright (c) 2011 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/bookmarks/bookmark_utils_gtk.h"
6
7#include "base/pickle.h"
8#include "base/string16.h"
9#include "base/string_util.h"
10#include "base/utf_string_conversions.h"
11#include "chrome/browser/bookmarks/bookmark_model.h"
12#include "chrome/browser/bookmarks/bookmark_node_data.h"
13#include "chrome/browser/bookmarks/bookmark_utils.h"
14#include "chrome/browser/profiles/profile.h"
15#include "chrome/browser/ui/gtk/gtk_chrome_button.h"
16#include "chrome/browser/ui/gtk/gtk_theme_service.h"
17#include "chrome/browser/ui/gtk/gtk_util.h"
18#include "ui/base/dragdrop/gtk_dnd_util.h"
19#include "ui/base/l10n/l10n_util.h"
20#include "ui/base/resource/resource_bundle.h"
21#include "ui/gfx/canvas_skia_paint.h"
22#include "ui/gfx/font.h"
23#include "ui/gfx/gtk_util.h"
24
25namespace {
26
27// Spacing between the favicon and the text.
28const int kBarButtonPadding = 4;
29
30// Used in gtk_selection_data_set(). (I assume from this parameter that gtk has
31// to some really exotic hardware...)
32const int kBitsInAByte = 8;
33
34// Maximum number of characters on a bookmark button.
35const size_t kMaxCharsOnAButton = 15;
36
37// Max size of each component of the button tooltips.
38const size_t kMaxTooltipTitleLength = 100;
39const size_t kMaxTooltipURLLength = 400;
40
41// Padding between the chrome button highlight border and the contents (favicon,
42// text).
43const int kButtonPaddingTop = 0;
44const int kButtonPaddingBottom = 0;
45const int kButtonPaddingLeft = 5;
46const int kButtonPaddingRight = 0;
47
48void* AsVoid(const BookmarkNode* node) {
49  return const_cast<BookmarkNode*>(node);
50}
51
52// Creates the widget hierarchy for a bookmark button.
53void PackButton(GdkPixbuf* pixbuf, const string16& title, bool ellipsize,
54                GtkThemeService* provider, GtkWidget* button) {
55  GtkWidget* former_child = gtk_bin_get_child(GTK_BIN(button));
56  if (former_child)
57    gtk_container_remove(GTK_CONTAINER(button), former_child);
58
59  // We pack the button manually (rather than using gtk_button_set_*) so that
60  // we can have finer control over its label.
61  GtkWidget* image = gtk_image_new_from_pixbuf(pixbuf);
62
63  GtkWidget* box = gtk_hbox_new(FALSE, kBarButtonPadding);
64  gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0);
65
66  std::string label_string = UTF16ToUTF8(title);
67  if (!label_string.empty()) {
68    GtkWidget* label = gtk_label_new(label_string.c_str());
69    // Until we switch to vector graphics, force the font size.
70    gtk_util::ForceFontSizePixels(label, 13.4);  // 13.4px == 10pt @ 96dpi
71
72    // Ellipsize long bookmark names.
73    if (ellipsize) {
74      gtk_label_set_max_width_chars(GTK_LABEL(label), kMaxCharsOnAButton);
75      gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_END);
76    }
77
78    gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0);
79    bookmark_utils::SetButtonTextColors(label, provider);
80  }
81
82  GtkWidget* alignment = gtk_alignment_new(0.0, 0.0, 1.0, 1.0);
83  // If we are not showing the label, don't set any padding, so that the icon
84  // will just be centered.
85  if (label_string.c_str()) {
86    gtk_alignment_set_padding(GTK_ALIGNMENT(alignment),
87        kButtonPaddingTop, kButtonPaddingBottom,
88        kButtonPaddingLeft, kButtonPaddingRight);
89  }
90  gtk_container_add(GTK_CONTAINER(alignment), box);
91  gtk_container_add(GTK_CONTAINER(button), alignment);
92
93  gtk_widget_show_all(alignment);
94}
95
96const int kDragRepresentationWidth = 140;
97
98struct DragRepresentationData {
99 public:
100  GdkPixbuf* favicon;
101  string16 text;
102  SkColor text_color;
103
104  DragRepresentationData(GdkPixbuf* favicon,
105                         const string16& text,
106                         SkColor text_color)
107      : favicon(favicon),
108        text(text),
109        text_color(text_color) {
110    g_object_ref(favicon);
111  }
112
113  ~DragRepresentationData() {
114    g_object_unref(favicon);
115  }
116
117 private:
118  DISALLOW_COPY_AND_ASSIGN(DragRepresentationData);
119};
120
121gboolean OnDragIconExpose(GtkWidget* sender,
122                          GdkEventExpose* event,
123                          DragRepresentationData* data) {
124  // Clear the background.
125  cairo_t* cr = gdk_cairo_create(event->window);
126  gdk_cairo_rectangle(cr, &event->area);
127  cairo_clip(cr);
128  cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
129  cairo_paint(cr);
130
131  cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
132  gdk_cairo_set_source_pixbuf(cr, data->favicon, 0, 0);
133  cairo_paint(cr);
134  cairo_destroy(cr);
135
136  // Paint the title text.
137  gfx::CanvasSkiaPaint canvas(event, false);
138  int text_x = gdk_pixbuf_get_width(data->favicon) + kBarButtonPadding;
139  int text_width = sender->allocation.width - text_x;
140  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
141  const gfx::Font& base_font = rb.GetFont(ResourceBundle::BaseFont);
142  canvas.DrawStringInt(data->text, base_font, data->text_color,
143                       text_x, 0, text_width, sender->allocation.height);
144
145  return TRUE;
146}
147
148void OnDragIconDestroy(GtkWidget* drag_icon,
149                       DragRepresentationData* data) {
150  g_object_unref(drag_icon);
151  delete data;
152}
153
154}  // namespace
155
156namespace bookmark_utils {
157
158const char kBookmarkNode[] = "bookmark-node";
159
160GdkPixbuf* GetPixbufForNode(const BookmarkNode* node, BookmarkModel* model,
161                            bool native) {
162  GdkPixbuf* pixbuf;
163
164  if (node->is_url()) {
165    if (model->GetFavicon(node).width() != 0) {
166      pixbuf = gfx::GdkPixbufFromSkBitmap(&model->GetFavicon(node));
167    } else {
168      pixbuf = GtkThemeService::GetDefaultFavicon(native);
169      g_object_ref(pixbuf);
170    }
171  } else {
172    pixbuf = GtkThemeService::GetFolderIcon(native);
173    g_object_ref(pixbuf);
174  }
175
176  return pixbuf;
177}
178
179GtkWidget* GetDragRepresentation(GdkPixbuf* pixbuf,
180                                 const string16& title,
181                                 GtkThemeService* provider) {
182  GtkWidget* window = gtk_window_new(GTK_WINDOW_POPUP);
183
184  if (gtk_util::IsScreenComposited() &&
185      gtk_util::AddWindowAlphaChannel(window)) {
186    DragRepresentationData* data = new DragRepresentationData(
187        pixbuf, title,
188        provider->GetColor(ThemeService::COLOR_BOOKMARK_TEXT));
189    g_signal_connect(window, "expose-event", G_CALLBACK(OnDragIconExpose),
190                     data);
191    g_object_ref(window);
192    g_signal_connect(window, "destroy", G_CALLBACK(OnDragIconDestroy), data);
193
194    ResourceBundle& rb = ResourceBundle::GetSharedInstance();
195    const gfx::Font& base_font = rb.GetFont(ResourceBundle::BaseFont);
196    gtk_widget_set_size_request(window, kDragRepresentationWidth,
197                                base_font.GetHeight());
198  } else {
199    if (!provider->UseGtkTheme()) {
200      GdkColor color = provider->GetGdkColor(
201          ThemeService::COLOR_TOOLBAR);
202      gtk_widget_modify_bg(window, GTK_STATE_NORMAL, &color);
203    }
204    gtk_widget_realize(window);
205
206    GtkWidget* frame = gtk_frame_new(NULL);
207    gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_OUT);
208    gtk_container_add(GTK_CONTAINER(window), frame);
209
210    GtkWidget* floating_button = provider->BuildChromeButton();
211    PackButton(pixbuf, title, true, provider, floating_button);
212    gtk_container_add(GTK_CONTAINER(frame), floating_button);
213    gtk_widget_show_all(frame);
214  }
215
216  return window;
217}
218
219GtkWidget* GetDragRepresentationForNode(const BookmarkNode* node,
220                                        BookmarkModel* model,
221                                        GtkThemeService* provider) {
222  GdkPixbuf* pixbuf = GetPixbufForNode(node, model, provider->UseGtkTheme());
223  GtkWidget* widget = GetDragRepresentation(pixbuf, node->GetTitle(), provider);
224  g_object_unref(pixbuf);
225  return widget;
226}
227
228void ConfigureButtonForNode(const BookmarkNode* node, BookmarkModel* model,
229                            GtkWidget* button, GtkThemeService* provider) {
230  GdkPixbuf* pixbuf = bookmark_utils::GetPixbufForNode(node, model,
231                                                       provider->UseGtkTheme());
232  PackButton(pixbuf, node->GetTitle(), node != model->other_node(), provider,
233             button);
234  g_object_unref(pixbuf);
235
236  std::string tooltip = BuildTooltipFor(node);
237  if (!tooltip.empty())
238    gtk_widget_set_tooltip_markup(button, tooltip.c_str());
239
240  g_object_set_data(G_OBJECT(button), bookmark_utils::kBookmarkNode,
241                    AsVoid(node));
242}
243
244std::string BuildTooltipFor(const BookmarkNode* node) {
245  if (node->is_folder())
246    return std::string();
247
248  const std::string& url = node->GetURL().possibly_invalid_spec();
249  const std::string& title = UTF16ToUTF8(node->GetTitle());
250
251  std::string truncated_url = UTF16ToUTF8(l10n_util::TruncateString(
252      UTF8ToUTF16(url), kMaxTooltipURLLength));
253  gchar* escaped_url_cstr = g_markup_escape_text(truncated_url.c_str(),
254                                                 truncated_url.size());
255  std::string escaped_url(escaped_url_cstr);
256  g_free(escaped_url_cstr);
257
258  std::string tooltip;
259  if (url == title || title.empty()) {
260    return escaped_url;
261  } else {
262    std::string truncated_title = UTF16ToUTF8(l10n_util::TruncateString(
263        node->GetTitle(), kMaxTooltipTitleLength));
264    gchar* escaped_title_cstr = g_markup_escape_text(truncated_title.c_str(),
265                                                     truncated_title.size());
266    std::string escaped_title(escaped_title_cstr);
267    g_free(escaped_title_cstr);
268
269    if (!escaped_url.empty())
270      return std::string("<b>") + escaped_title + "</b>\n" + escaped_url;
271    else
272      return std::string("<b>") + escaped_title + "</b>";
273  }
274}
275
276const BookmarkNode* BookmarkNodeForWidget(GtkWidget* widget) {
277  return reinterpret_cast<const BookmarkNode*>(
278      g_object_get_data(G_OBJECT(widget), bookmark_utils::kBookmarkNode));
279}
280
281void SetButtonTextColors(GtkWidget* label, GtkThemeService* provider) {
282  if (provider->UseGtkTheme()) {
283    gtk_util::SetLabelColor(label, NULL);
284  } else {
285    GdkColor color = provider->GetGdkColor(
286        ThemeService::COLOR_BOOKMARK_TEXT);
287    gtk_widget_modify_fg(label, GTK_STATE_NORMAL, &color);
288    gtk_widget_modify_fg(label, GTK_STATE_INSENSITIVE, &color);
289
290    // Because the prelight state is a white image that doesn't change by the
291    // theme, force the text color to black when it would be used.
292    gtk_widget_modify_fg(label, GTK_STATE_ACTIVE, &gtk_util::kGdkBlack);
293    gtk_widget_modify_fg(label, GTK_STATE_PRELIGHT, &gtk_util::kGdkBlack);
294  }
295}
296
297// DnD-related -----------------------------------------------------------------
298
299int GetCodeMask(bool folder) {
300  int rv = ui::CHROME_BOOKMARK_ITEM;
301  if (!folder) {
302    rv |= ui::TEXT_URI_LIST |
303          ui::TEXT_PLAIN |
304          ui::NETSCAPE_URL;
305  }
306  return rv;
307}
308
309void WriteBookmarkToSelection(const BookmarkNode* node,
310                              GtkSelectionData* selection_data,
311                              guint target_type,
312                              Profile* profile) {
313  DCHECK(node);
314  std::vector<const BookmarkNode*> nodes;
315  nodes.push_back(node);
316  WriteBookmarksToSelection(nodes, selection_data, target_type, profile);
317}
318
319void WriteBookmarksToSelection(const std::vector<const BookmarkNode*>& nodes,
320                               GtkSelectionData* selection_data,
321                               guint target_type,
322                               Profile* profile) {
323  switch (target_type) {
324    case ui::CHROME_BOOKMARK_ITEM: {
325      BookmarkNodeData data(nodes);
326      Pickle pickle;
327      data.WriteToPickle(profile, &pickle);
328
329      gtk_selection_data_set(selection_data, selection_data->target,
330                             kBitsInAByte,
331                             static_cast<const guchar*>(pickle.data()),
332                             pickle.size());
333      break;
334    }
335    case ui::NETSCAPE_URL: {
336      // _NETSCAPE_URL format is URL + \n + title.
337      std::string utf8_text = nodes[0]->GetURL().spec() + "\n" +
338          UTF16ToUTF8(nodes[0]->GetTitle());
339      gtk_selection_data_set(selection_data,
340                             selection_data->target,
341                             kBitsInAByte,
342                             reinterpret_cast<const guchar*>(utf8_text.c_str()),
343                             utf8_text.length());
344      break;
345    }
346    case ui::TEXT_URI_LIST: {
347      gchar** uris = reinterpret_cast<gchar**>(malloc(sizeof(gchar*) *
348                                               (nodes.size() + 1)));
349      for (size_t i = 0; i < nodes.size(); ++i) {
350        // If the node is a folder, this will be empty. TODO(estade): figure out
351        // if there are any ramifications to passing an empty URI. After a
352        // little testing, it seems fine.
353        const GURL& url = nodes[i]->GetURL();
354        // This const cast should be safe as gtk_selection_data_set_uris()
355        // makes copies.
356        uris[i] = const_cast<gchar*>(url.spec().c_str());
357      }
358      uris[nodes.size()] = NULL;
359
360      gtk_selection_data_set_uris(selection_data, uris);
361      free(uris);
362      break;
363    }
364    case ui::TEXT_PLAIN: {
365      gtk_selection_data_set_text(selection_data,
366                                  nodes[0]->GetURL().spec().c_str(), -1);
367      break;
368    }
369    default: {
370      DLOG(ERROR) << "Unsupported drag get type!";
371    }
372  }
373}
374
375std::vector<const BookmarkNode*> GetNodesFromSelection(
376    GdkDragContext* context,
377    GtkSelectionData* selection_data,
378    guint target_type,
379    Profile* profile,
380    gboolean* delete_selection_data,
381    gboolean* dnd_success) {
382  if (delete_selection_data)
383    *delete_selection_data = FALSE;
384  if (dnd_success)
385    *dnd_success = FALSE;
386
387  if (selection_data && selection_data->length > 0) {
388    if (context && delete_selection_data && context->action == GDK_ACTION_MOVE)
389      *delete_selection_data = TRUE;
390
391    switch (target_type) {
392      case ui::CHROME_BOOKMARK_ITEM: {
393        if (dnd_success)
394          *dnd_success = TRUE;
395        Pickle pickle(reinterpret_cast<char*>(selection_data->data),
396                      selection_data->length);
397        BookmarkNodeData drag_data;
398        drag_data.ReadFromPickle(&pickle);
399        return drag_data.GetNodes(profile);
400      }
401      default: {
402        DLOG(ERROR) << "Unsupported drag received type: " << target_type;
403      }
404    }
405  }
406
407  return std::vector<const BookmarkNode*>();
408}
409
410bool CreateNewBookmarkFromNamedUrl(GtkSelectionData* selection_data,
411    BookmarkModel* model, const BookmarkNode* parent, int idx) {
412  GURL url;
413  string16 title;
414  if (!ui::ExtractNamedURL(selection_data, &url, &title))
415    return false;
416
417  model->AddURL(parent, idx, title, url);
418  return true;
419}
420
421bool CreateNewBookmarksFromURIList(GtkSelectionData* selection_data,
422    BookmarkModel* model, const BookmarkNode* parent, int idx) {
423  std::vector<GURL> urls;
424  ui::ExtractURIList(selection_data, &urls);
425  for (size_t i = 0; i < urls.size(); ++i) {
426    string16 title = GetNameForURL(urls[i]);
427    model->AddURL(parent, idx++, title, urls[i]);
428  }
429  return true;
430}
431
432bool CreateNewBookmarkFromNetscapeURL(GtkSelectionData* selection_data,
433    BookmarkModel* model, const BookmarkNode* parent, int idx) {
434  GURL url;
435  string16 title;
436  if (!ui::ExtractNetscapeURL(selection_data, &url, &title))
437    return false;
438
439  model->AddURL(parent, idx, title, url);
440  return true;
441}
442
443}  // namespace bookmark_utils
444