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