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