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