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 25 namespace { 26 27 // Spacing between the favicon and the text. 28 const 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...) 32 const int kBitsInAByte = 8; 33 34 // Maximum number of characters on a bookmark button. 35 const size_t kMaxCharsOnAButton = 15; 36 37 // Max size of each component of the button tooltips. 38 const size_t kMaxTooltipTitleLength = 100; 39 const size_t kMaxTooltipURLLength = 400; 40 41 // Padding between the chrome button highlight border and the contents (favicon, 42 // text). 43 const int kButtonPaddingTop = 0; 44 const int kButtonPaddingBottom = 0; 45 const int kButtonPaddingLeft = 5; 46 const int kButtonPaddingRight = 0; 47 48 void* AsVoid(const BookmarkNode* node) { 49 return const_cast<BookmarkNode*>(node); 50 } 51 52 // Creates the widget hierarchy for a bookmark button. 53 void 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 96 const int kDragRepresentationWidth = 140; 97 98 struct 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 121 gboolean 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 148 void OnDragIconDestroy(GtkWidget* drag_icon, 149 DragRepresentationData* data) { 150 g_object_unref(drag_icon); 151 delete data; 152 } 153 154 } // namespace 155 156 namespace bookmark_utils { 157 158 const char kBookmarkNode[] = "bookmark-node"; 159 160 GdkPixbuf* 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 179 GtkWidget* 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 219 GtkWidget* 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 228 void 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 244 std::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 276 const BookmarkNode* BookmarkNodeForWidget(GtkWidget* widget) { 277 return reinterpret_cast<const BookmarkNode*>( 278 g_object_get_data(G_OBJECT(widget), bookmark_utils::kBookmarkNode)); 279 } 280 281 void 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, >k_util::kGdkBlack); 293 gtk_widget_modify_fg(label, GTK_STATE_PRELIGHT, >k_util::kGdkBlack); 294 } 295 } 296 297 // DnD-related ----------------------------------------------------------------- 298 299 int 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 309 void 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 319 void 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 375 std::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 410 bool 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 421 bool 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 432 bool 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