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