1/* 2 * This file Copyright (C) Mnemosyne LLC 3 * 4 * This file is licensed by the GPL version 2. Works owned by the 5 * Transmission project are granted a special exemption to clause 2(b) 6 * so that the bulk of its code can remain under the MIT license. 7 * This exemption does not extend to derived works not owned by 8 * the Transmission project. 9 * 10 * $Id: util.c 13560 2012-10-13 16:47:26Z jordan $ 11 */ 12 13#include <ctype.h> /* isxdigit() */ 14#include <errno.h> 15#include <stdarg.h> 16#include <string.h> /* strchr(), strrchr(), strlen(), strstr() */ 17 18#include <gtk/gtk.h> 19#include <glib/gi18n.h> 20#include <glib/gstdio.h> /* g_unlink() */ 21#include <gio/gio.h> /* g_file_trash() */ 22 23#include <libtransmission/transmission.h> /* TR_RATIO_NA, TR_RATIO_INF */ 24#include <libtransmission/utils.h> /* tr_strratio() */ 25#include <libtransmission/web.h> /* tr_webResponseStr() */ 26#include <libtransmission/version.h> /* SHORT_VERSION_STRING */ 27 28#include "conf.h" 29#include "hig.h" 30#include "tr-prefs.h" 31#include "util.h" 32 33/*** 34**** UNITS 35***/ 36 37const int mem_K = 1024; 38const char * mem_K_str = N_("KiB"); 39const char * mem_M_str = N_("MiB"); 40const char * mem_G_str = N_("GiB"); 41const char * mem_T_str = N_("TiB"); 42 43const int disk_K = 1000; 44const char * disk_K_str = N_("kB"); 45const char * disk_M_str = N_("MB"); 46const char * disk_G_str = N_("GB"); 47const char * disk_T_str = N_("TB"); 48 49const int speed_K = 1000; 50const char * speed_K_str = N_("kB/s"); 51const char * speed_M_str = N_("MB/s"); 52const char * speed_G_str = N_("GB/s"); 53const char * speed_T_str = N_("TB/s"); 54 55/*** 56**** 57***/ 58 59const char* 60gtr_get_unicode_string( int i ) 61{ 62 switch( i ) { 63 case GTR_UNICODE_UP: return "\xE2\x86\x91"; 64 case GTR_UNICODE_DOWN: return "\xE2\x86\x93"; 65 case GTR_UNICODE_INF: return "\xE2\x88\x9E"; 66 case GTR_UNICODE_BULLET: return "\xE2\x88\x99"; 67 default: return "err"; 68 } 69} 70 71char* 72tr_strlratio( char * buf, double ratio, size_t buflen ) 73{ 74 return tr_strratio( buf, buflen, ratio, gtr_get_unicode_string( GTR_UNICODE_INF ) ); 75} 76 77char* 78tr_strlpercent( char * buf, double x, size_t buflen ) 79{ 80 return tr_strpercent( buf, x, buflen ); 81} 82 83char* 84tr_strlsize( char * buf, guint64 bytes, size_t buflen ) 85{ 86 if( !bytes ) 87 g_strlcpy( buf, Q_( "None" ), buflen ); 88 else 89 tr_formatter_size_B( buf, bytes, buflen ); 90 91 return buf; 92} 93 94char* 95tr_strltime( char * buf, int seconds, size_t buflen ) 96{ 97 int days, hours, minutes; 98 char d[128], h[128], m[128], s[128]; 99 100 if( seconds < 0 ) 101 seconds = 0; 102 103 days = seconds / 86400; 104 hours = ( seconds % 86400 ) / 3600; 105 minutes = ( seconds % 3600 ) / 60; 106 seconds = ( seconds % 3600 ) % 60; 107 108 g_snprintf( d, sizeof( d ), ngettext( "%'d day", "%'d days", days ), days ); 109 g_snprintf( h, sizeof( h ), ngettext( "%'d hour", "%'d hours", hours ), hours ); 110 g_snprintf( m, sizeof( m ), ngettext( "%'d minute", "%'d minutes", minutes ), minutes ); 111 g_snprintf( s, sizeof( s ), ngettext( "%'d second", "%'d seconds", seconds ), seconds ); 112 113 if( days ) 114 { 115 if( days >= 4 || !hours ) 116 g_strlcpy( buf, d, buflen ); 117 else 118 g_snprintf( buf, buflen, "%s, %s", d, h ); 119 } 120 else if( hours ) 121 { 122 if( hours >= 4 || !minutes ) 123 g_strlcpy( buf, h, buflen ); 124 else 125 g_snprintf( buf, buflen, "%s, %s", h, m ); 126 } 127 else if( minutes ) 128 { 129 if( minutes >= 4 || !seconds ) 130 g_strlcpy( buf, m, buflen ); 131 else 132 g_snprintf( buf, buflen, "%s, %s", m, s ); 133 } 134 else 135 { 136 g_strlcpy( buf, s, buflen ); 137 } 138 139 return buf; 140} 141 142/* pattern-matching text; ie, legaltorrents.com */ 143void 144gtr_get_host_from_url( char * buf, size_t buflen, const char * url ) 145{ 146 char host[1024]; 147 const char * pch; 148 149 if(( pch = strstr( url, "://" ))) { 150 const size_t hostlen = strcspn( pch+3, ":/" ); 151 const size_t copylen = MIN( hostlen, sizeof(host)-1 ); 152 memcpy( host, pch+3, copylen ); 153 host[copylen] = '\0'; 154 } else { 155 *host = '\0'; 156 } 157 158 if( tr_addressIsIP( host ) ) 159 g_strlcpy( buf, url, buflen ); 160 else { 161 const char * first_dot = strchr( host, '.' ); 162 const char * last_dot = strrchr( host, '.' ); 163 if( ( first_dot ) && ( last_dot ) && ( first_dot != last_dot ) ) 164 g_strlcpy( buf, first_dot + 1, buflen ); 165 else 166 g_strlcpy( buf, host, buflen ); 167 } 168} 169 170static gboolean 171gtr_is_supported_url( const char * str ) 172{ 173 return ( ( str != NULL ) && 174 ( g_str_has_prefix( str, "ftp://" ) || 175 g_str_has_prefix( str, "http://" ) || 176 g_str_has_prefix( str, "https://" ) ) ); 177} 178 179gboolean 180gtr_is_magnet_link( const char * str ) 181{ 182 return ( str != NULL ) && 183 ( g_str_has_prefix( str, "magnet:?" ) ); 184} 185 186gboolean 187gtr_is_hex_hashcode( const char * str ) 188{ 189 int i; 190 191 if( !str || ( strlen( str ) != 40 ) ) 192 return FALSE; 193 194 for( i=0; i<40; ++i ) 195 if( !isxdigit( str[i] ) ) 196 return FALSE; 197 198 return TRUE; 199} 200 201static GtkWindow * 202getWindow( GtkWidget * w ) 203{ 204 if( w == NULL ) 205 return NULL; 206 207 if( GTK_IS_WINDOW( w ) ) 208 return GTK_WINDOW( w ); 209 210 return GTK_WINDOW( gtk_widget_get_ancestor( w, GTK_TYPE_WINDOW ) ); 211} 212 213void 214gtr_add_torrent_error_dialog( GtkWidget * child, int err, const char * file ) 215{ 216 char * secondary; 217 const char * fmt; 218 GtkWidget * w; 219 GtkWindow * win = getWindow( child ); 220 221 switch( err ) 222 { 223 case TR_PARSE_ERR: fmt = _( "The torrent file \"%s\" contains invalid data." ); break; 224 case TR_PARSE_DUPLICATE: fmt = _( "The torrent file \"%s\" is already in use." ); break; 225 default: fmt = _( "The torrent file \"%s\" encountered an unknown error." ); break; 226 } 227 secondary = g_strdup_printf( fmt, file ); 228 229 w = gtk_message_dialog_new( win, 230 GTK_DIALOG_DESTROY_WITH_PARENT, 231 GTK_MESSAGE_ERROR, 232 GTK_BUTTONS_CLOSE, 233 "%s", _( "Error opening torrent" ) ); 234 gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( w ), 235 "%s", secondary ); 236 g_signal_connect_swapped( w, "response", 237 G_CALLBACK( gtk_widget_destroy ), w ); 238 gtk_widget_show_all( w ); 239 g_free( secondary ); 240} 241 242typedef void ( PopupFunc )( GtkWidget*, GdkEventButton* ); 243 244/* pop up the context menu if a user right-clicks. 245 if the row they right-click on isn't selected, select it. */ 246 247gboolean 248on_tree_view_button_pressed( GtkWidget * view, 249 GdkEventButton * event, 250 gpointer func ) 251{ 252 GtkTreeView * tv = GTK_TREE_VIEW( view ); 253 254 if( event->type == GDK_BUTTON_PRESS && event->button == 3 ) 255 { 256 GtkTreeSelection * selection = gtk_tree_view_get_selection( tv ); 257 GtkTreePath * path; 258 if( gtk_tree_view_get_path_at_pos ( tv, 259 (gint) event->x, 260 (gint) event->y, 261 &path, NULL, NULL, NULL ) ) 262 { 263 if( !gtk_tree_selection_path_is_selected ( selection, path ) ) 264 { 265 gtk_tree_selection_unselect_all ( selection ); 266 gtk_tree_selection_select_path ( selection, path ); 267 } 268 gtk_tree_path_free( path ); 269 } 270 271 if( func != NULL ) 272 ( (PopupFunc*)func )( view, event ); 273 274 return TRUE; 275 } 276 277 return FALSE; 278} 279 280/* if the user clicked in an empty area of the list, 281 * clear all the selections. */ 282gboolean 283on_tree_view_button_released( GtkWidget * view, 284 GdkEventButton * event, 285 gpointer unused UNUSED ) 286{ 287 GtkTreeView * tv = GTK_TREE_VIEW( view ); 288 289 if( !gtk_tree_view_get_path_at_pos ( tv, 290 (gint) event->x, 291 (gint) event->y, 292 NULL, NULL, NULL, NULL ) ) 293 { 294 GtkTreeSelection * selection = gtk_tree_view_get_selection( tv ); 295 gtk_tree_selection_unselect_all ( selection ); 296 } 297 298 return FALSE; 299} 300 301int 302gtr_file_trash_or_remove( const char * filename ) 303{ 304 GFile * file; 305 gboolean trashed = FALSE; 306 307 g_return_val_if_fail (filename && *filename, 0); 308 309 file = g_file_new_for_path( filename ); 310 311 if( gtr_pref_flag_get( PREF_KEY_TRASH_CAN_ENABLED ) ) { 312 GError * err = NULL; 313 trashed = g_file_trash( file, NULL, &err ); 314 if( err ) { 315 g_message( "Unable to trash file \"%s\": %s", filename, err->message ); 316 g_clear_error( &err ); 317 } 318 } 319 320 if( !trashed ) { 321 GError * err = NULL; 322 g_file_delete( file, NULL, &err ); 323 if( err ) { 324 g_message( "Unable to delete file \"%s\": %s", filename, err->message ); 325 g_clear_error( &err ); 326 } 327 } 328 329 g_object_unref( G_OBJECT( file ) ); 330 return 0; 331} 332 333const char* 334gtr_get_help_uri( void ) 335{ 336 static char * uri = NULL; 337 338 if( !uri ) 339 { 340 int major, minor; 341 const char * fmt = "http://www.transmissionbt.com/help/gtk/%d.%dx"; 342 sscanf( SHORT_VERSION_STRING, "%d.%d", &major, &minor ); 343 uri = g_strdup_printf( fmt, major, minor / 10 ); 344 } 345 346 return uri; 347} 348 349void 350gtr_open_file( const char * path ) 351{ 352 char * uri; 353 354 GFile * file = g_file_new_for_path( path ); 355 g_object_unref( G_OBJECT( file ) ); 356 357 if( g_path_is_absolute( path ) ) 358 uri = g_strdup_printf( "file://%s", path ); 359 else { 360 char * cwd = g_get_current_dir(); 361 uri = g_strdup_printf( "file://%s/%s", cwd, path ); 362 g_free( cwd ); 363 } 364 365 gtr_open_uri( uri ); 366 g_free( uri ); 367} 368 369void 370gtr_open_uri( const char * uri ) 371{ 372 if( uri ) 373 { 374 gboolean opened = FALSE; 375 376 if( !opened ) 377 opened = gtk_show_uri( NULL, uri, GDK_CURRENT_TIME, NULL ); 378 379 if( !opened ) 380 opened = g_app_info_launch_default_for_uri( uri, NULL, NULL ); 381 382 if( !opened ) { 383 char * argv[] = { (char*)"xdg-open", (char*)uri, NULL }; 384 opened = g_spawn_async( NULL, argv, NULL, G_SPAWN_SEARCH_PATH, 385 NULL, NULL, NULL, NULL ); 386 } 387 388 if( !opened ) 389 g_message( "Unable to open \"%s\"", uri ); 390 } 391} 392 393/*** 394**** 395***/ 396 397void 398gtr_combo_box_set_active_enum( GtkComboBox * combo_box, int value ) 399{ 400 int i; 401 int currentValue; 402 const int column = 0; 403 GtkTreeIter iter; 404 GtkTreeModel * model = gtk_combo_box_get_model( combo_box ); 405 406 /* do the value and current value match? */ 407 if( gtk_combo_box_get_active_iter( combo_box, &iter ) ) { 408 gtk_tree_model_get( model, &iter, column, ¤tValue, -1 ); 409 if( currentValue == value ) 410 return; 411 } 412 413 /* find the one to select */ 414 i = 0; 415 while(( gtk_tree_model_iter_nth_child( model, &iter, NULL, i++ ))) { 416 gtk_tree_model_get( model, &iter, column, ¤tValue, -1 ); 417 if( currentValue == value ) { 418 gtk_combo_box_set_active_iter( combo_box, &iter ); 419 return; 420 } 421 } 422} 423 424 425GtkWidget * 426gtr_combo_box_new_enum( const char * text_1, ... ) 427{ 428 GtkWidget * w; 429 GtkCellRenderer * r; 430 GtkListStore * store; 431 va_list vl; 432 const char * text; 433 va_start( vl, text_1 ); 434 435 store = gtk_list_store_new( 2, G_TYPE_INT, G_TYPE_STRING ); 436 437 text = text_1; 438 if( text != NULL ) do 439 { 440 const int val = va_arg( vl, int ); 441 gtk_list_store_insert_with_values( store, NULL, INT_MAX, 0, val, 1, text, -1 ); 442 text = va_arg( vl, const char * ); 443 } 444 while( text != NULL ); 445 446 w = gtk_combo_box_new_with_model( GTK_TREE_MODEL( store ) ); 447 r = gtk_cell_renderer_text_new( ); 448 gtk_cell_layout_pack_start( GTK_CELL_LAYOUT( w ), r, TRUE ); 449 gtk_cell_layout_set_attributes( GTK_CELL_LAYOUT( w ), r, "text", 1, NULL ); 450 451 /* cleanup */ 452 g_object_unref( store ); 453 return w; 454} 455 456int 457gtr_combo_box_get_active_enum( GtkComboBox * combo_box ) 458{ 459 int value = 0; 460 GtkTreeIter iter; 461 462 if( gtk_combo_box_get_active_iter( combo_box, &iter ) ) 463 gtk_tree_model_get( gtk_combo_box_get_model( combo_box ), &iter, 0, &value, -1 ); 464 465 return value; 466} 467 468GtkWidget * 469gtr_priority_combo_new( void ) 470{ 471 return gtr_combo_box_new_enum( _( "High" ), TR_PRI_HIGH, 472 _( "Normal" ), TR_PRI_NORMAL, 473 _( "Low" ), TR_PRI_LOW, 474 NULL ); 475} 476 477/*** 478**** 479***/ 480 481#define GTR_CHILD_HIDDEN "gtr-child-hidden" 482 483void 484gtr_widget_set_visible( GtkWidget * w, gboolean b ) 485{ 486 /* toggle the transient children, too */ 487 if( GTK_IS_WINDOW( w ) ) 488 { 489 GList * l; 490 GList * windows = gtk_window_list_toplevels( ); 491 GtkWindow * window = GTK_WINDOW( w ); 492 493 for( l=windows; l!=NULL; l=l->next ) 494 { 495 if( !GTK_IS_WINDOW( l->data ) ) 496 continue; 497 if( gtk_window_get_transient_for( GTK_WINDOW( l->data ) ) != window ) 498 continue; 499 if( gtk_widget_get_visible( GTK_WIDGET( l->data ) ) == b ) 500 continue; 501 502 if( b && g_object_get_data( G_OBJECT( l->data ), GTR_CHILD_HIDDEN ) != NULL ) 503 { 504 g_object_steal_data( G_OBJECT( l->data ), GTR_CHILD_HIDDEN ); 505 gtr_widget_set_visible( GTK_WIDGET( l->data ), TRUE ); 506 } 507 else if( !b ) 508 { 509 g_object_set_data( G_OBJECT( l->data ), GTR_CHILD_HIDDEN, GINT_TO_POINTER( 1 ) ); 510 gtr_widget_set_visible( GTK_WIDGET( l->data ), FALSE ); 511 } 512 } 513 514 g_list_free( windows ); 515 } 516 517 gtk_widget_set_visible( w, b ); 518} 519 520void 521gtr_dialog_set_content( GtkDialog * dialog, GtkWidget * content ) 522{ 523 GtkWidget * vbox = gtk_dialog_get_content_area( dialog ); 524 gtk_box_pack_start( GTK_BOX( vbox ), content, TRUE, TRUE, 0 ); 525 gtk_widget_show_all( content ); 526} 527 528/*** 529**** 530***/ 531 532void 533gtr_http_failure_dialog( GtkWidget * parent, const char * url, long response_code ) 534{ 535 GtkWindow * window = getWindow( parent ); 536 537 GtkWidget * w = gtk_message_dialog_new( window, 0, 538 GTK_MESSAGE_ERROR, 539 GTK_BUTTONS_CLOSE, 540 _( "Error opening \"%s\"" ), url ); 541 542 gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( w ), 543 _( "Server returned \"%1$ld %2$s\"" ), 544 response_code, 545 tr_webGetResponseStr( response_code ) ); 546 547 g_signal_connect_swapped( w, "response", G_CALLBACK( gtk_widget_destroy ), w ); 548 gtk_widget_show( w ); 549} 550 551void 552gtr_unrecognized_url_dialog( GtkWidget * parent, const char * url ) 553{ 554 const char * xt = "xt=urn:btih"; 555 556 GtkWindow * window = getWindow( parent ); 557 558 GString * gstr = g_string_new( NULL ); 559 560 GtkWidget * w = gtk_message_dialog_new( window, 0, 561 GTK_MESSAGE_ERROR, 562 GTK_BUTTONS_CLOSE, 563 "%s", _( "Unrecognized URL" ) ); 564 565 g_string_append_printf( gstr, _( "Transmission doesn't know how to use \"%s\"" ), url ); 566 567 if( gtr_is_magnet_link( url ) && ( strstr( url, xt ) == NULL ) ) 568 { 569 g_string_append_printf( gstr, "\n \n" ); 570 g_string_append_printf( gstr, _( "This magnet link appears to be intended for something other than BitTorrent. BitTorrent magnet links have a section containing \"%s\"." ), xt ); 571 } 572 573 gtk_message_dialog_format_secondary_text( GTK_MESSAGE_DIALOG( w ), "%s", gstr->str ); 574 g_signal_connect_swapped( w, "response", G_CALLBACK( gtk_widget_destroy ), w ); 575 gtk_widget_show( w ); 576 g_string_free( gstr, TRUE ); 577} 578 579/*** 580**** 581***/ 582 583void 584gtr_paste_clipboard_url_into_entry( GtkWidget * e ) 585{ 586 size_t i; 587 588 char * text[] = { 589 g_strstrip( gtk_clipboard_wait_for_text( gtk_clipboard_get( GDK_SELECTION_PRIMARY ) ) ), 590 g_strstrip( gtk_clipboard_wait_for_text( gtk_clipboard_get( GDK_SELECTION_CLIPBOARD ) ) ) 591 }; 592 593 for( i=0; i<G_N_ELEMENTS(text); ++i ) { 594 char * s = text[i]; 595 if( s && ( gtr_is_supported_url( s ) || gtr_is_magnet_link( s ) 596 || gtr_is_hex_hashcode( s ) ) ) { 597 gtk_entry_set_text( GTK_ENTRY( e ), s ); 598 break; 599 } 600 } 601 602 for( i=0; i<G_N_ELEMENTS(text); ++i ) 603 g_free( text[i] ); 604} 605 606/*** 607**** 608***/ 609 610void 611gtr_label_set_text( GtkLabel * lb, const char * newstr ) 612{ 613 const char * oldstr = gtk_label_get_text( lb ); 614 615 if( tr_strcmp0( oldstr, newstr ) ) 616 gtk_label_set_text( lb, newstr ); 617} 618