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: makemeta-ui.c 13388 2012-07-14 19:26:55Z jordan $ 11 */ 12 13#include <glib/gi18n.h> 14#include <gtk/gtk.h> 15 16#include <libtransmission/transmission.h> 17#include <libtransmission/makemeta.h> 18#include <libtransmission/utils.h> /* tr_formatter_mem_B() */ 19 20#include "hig.h" 21#include "makemeta-ui.h" 22#include "tr-core.h" 23#include "tr-prefs.h" 24#include "util.h" 25 26#define FILE_CHOSEN_KEY "file-is-chosen" 27 28typedef struct 29{ 30 char * target; 31 guint progress_tag; 32 GtkWidget * file_radio; 33 GtkWidget * file_chooser; 34 GtkWidget * folder_radio; 35 GtkWidget * folder_chooser; 36 GtkWidget * pieces_lb; 37 GtkWidget * destination_chooser; 38 GtkWidget * comment_check; 39 GtkWidget * comment_entry; 40 GtkWidget * private_check; 41 GtkWidget * progress_label; 42 GtkWidget * progress_bar; 43 GtkWidget * progress_dialog; 44 GtkWidget * dialog; 45 GtkTextBuffer * announce_text_buffer; 46 TrCore * core; 47 tr_metainfo_builder * builder; 48} 49MakeMetaUI; 50 51static void 52freeMetaUI( gpointer p ) 53{ 54 MakeMetaUI * ui = p; 55 56 tr_metaInfoBuilderFree( ui->builder ); 57 g_free( ui->target ); 58 memset( ui, ~0, sizeof( MakeMetaUI ) ); 59 g_free( ui ); 60} 61 62static gboolean 63onProgressDialogRefresh( gpointer data ) 64{ 65 char * str = NULL; 66 MakeMetaUI * ui = data; 67 const tr_metainfo_builder * b = ui->builder; 68 GtkDialog * d = GTK_DIALOG( ui->progress_dialog ); 69 GtkProgressBar * p = GTK_PROGRESS_BAR( ui->progress_bar ); 70 const double fraction = b->pieceCount ? ((double)b->pieceIndex / b->pieceCount) : 0; 71 char * base = g_path_get_basename( b->top ); 72 73 /* progress label */ 74 if( !b->isDone ) 75 str = g_strdup_printf( _( "Creating \"%s\"" ), base ); 76 else if( b->result == TR_MAKEMETA_OK ) 77 str = g_strdup_printf( _( "Created \"%s\"!" ), base ); 78 else if( b->result == TR_MAKEMETA_URL ) 79 str = g_strdup_printf( _( "Error: invalid announce URL \"%s\"" ), b->errfile ); 80 else if( b->result == TR_MAKEMETA_CANCELLED ) 81 str = g_strdup_printf( _( "Cancelled" ) ); 82 else if( b->result == TR_MAKEMETA_IO_READ ) 83 str = g_strdup_printf( _( "Error reading \"%s\": %s" ), b->errfile, g_strerror( b->my_errno ) ); 84 else if( b->result == TR_MAKEMETA_IO_WRITE ) 85 str = g_strdup_printf( _( "Error writing \"%s\": %s" ), b->errfile, g_strerror( b->my_errno ) ); 86 else 87 g_assert_not_reached( ); 88 89 if( str != NULL ) { 90 gtr_label_set_text( GTK_LABEL( ui->progress_label ), str ); 91 g_free( str ); 92 } 93 94 /* progress bar */ 95 if( !b->pieceIndex ) 96 str = g_strdup( "" ); 97 else { 98 char sizebuf[128]; 99 tr_strlsize( sizebuf, (uint64_t)b->pieceIndex * 100 (uint64_t)b->pieceSize, sizeof( sizebuf ) ); 101 /* how much data we've scanned through to generate checksums */ 102 str = g_strdup_printf( _( "Scanned %s" ), sizebuf ); 103 } 104 gtk_progress_bar_set_fraction( p, fraction ); 105 gtk_progress_bar_set_text( p, str ); 106 g_free( str ); 107 108 /* buttons */ 109 gtk_dialog_set_response_sensitive( d, GTK_RESPONSE_CANCEL, !b->isDone ); 110 gtk_dialog_set_response_sensitive( d, GTK_RESPONSE_CLOSE, b->isDone ); 111 gtk_dialog_set_response_sensitive( d, GTK_RESPONSE_ACCEPT, b->isDone && !b->result ); 112 113 g_free( base ); 114 return TRUE; 115} 116 117static void 118onProgressDialogDestroyed( gpointer data, GObject * dead UNUSED ) 119{ 120 MakeMetaUI * ui = data; 121 g_source_remove( ui->progress_tag ); 122} 123 124static void 125addTorrent( MakeMetaUI * ui ) 126{ 127 char * path; 128 const tr_metainfo_builder * b = ui->builder; 129 tr_ctor * ctor = tr_ctorNew( gtr_core_session( ui->core ) ); 130 131 tr_ctorSetMetainfoFromFile( ctor, ui->target ); 132 133 path = g_path_get_dirname( b->top ); 134 tr_ctorSetDownloadDir( ctor, TR_FORCE, path ); 135 g_free( path ); 136 137 gtr_core_add_ctor( ui->core, ctor ); 138} 139 140static void 141onProgressDialogResponse( GtkDialog * d, int response, gpointer data ) 142{ 143 MakeMetaUI * ui = data; 144 145 switch( response ) 146 { 147 case GTK_RESPONSE_CANCEL: 148 ui->builder->abortFlag = TRUE; 149 gtk_widget_destroy( GTK_WIDGET( d ) ); 150 break; 151 case GTK_RESPONSE_ACCEPT: 152 addTorrent( ui ); 153 /* fall-through */ 154 case GTK_RESPONSE_CLOSE: 155 gtk_widget_destroy( ui->builder->result ? GTK_WIDGET( d ) : ui->dialog ); 156 break; 157 default: 158 g_assert( 0 && "unhandled response" ); 159 } 160} 161 162static void 163makeProgressDialog( GtkWidget * parent, MakeMetaUI * ui ) 164{ 165 GtkWidget *d, *l, *w, *v, *fr; 166 167 d = gtk_dialog_new_with_buttons( _( "New Torrent" ), 168 GTK_WINDOW( parent ), 169 GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT, 170 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, 171 GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, 172 GTK_STOCK_ADD, GTK_RESPONSE_ACCEPT, 173 NULL ); 174 ui->progress_dialog = d; 175 g_signal_connect( d, "response", G_CALLBACK( onProgressDialogResponse ), ui ); 176 177 fr = gtk_frame_new( NULL ); 178 gtk_container_set_border_width( GTK_CONTAINER( fr ), GUI_PAD_BIG ); 179 gtk_frame_set_shadow_type( GTK_FRAME( fr ), GTK_SHADOW_NONE ); 180 v = gtk_box_new( GTK_ORIENTATION_VERTICAL, GUI_PAD ); 181 gtk_container_add( GTK_CONTAINER( fr ), v ); 182 183 l = gtk_label_new( _( "Creating torrent���" ) ); 184 gtk_misc_set_alignment( GTK_MISC( l ), 0.0, 0.5 ); 185 gtk_label_set_justify( GTK_LABEL( l ), GTK_JUSTIFY_LEFT ); 186 ui->progress_label = l; 187 gtk_box_pack_start( GTK_BOX( v ), l, FALSE, FALSE, 0 ); 188 189 w = gtk_progress_bar_new( ); 190 ui->progress_bar = w; 191 gtk_box_pack_start( GTK_BOX( v ), w, FALSE, FALSE, 0 ); 192 193 ui->progress_tag = gdk_threads_add_timeout_seconds( SECONDARY_WINDOW_REFRESH_INTERVAL_SECONDS, onProgressDialogRefresh, ui ); 194 g_object_weak_ref( G_OBJECT( d ), onProgressDialogDestroyed, ui ); 195 onProgressDialogRefresh( ui ); 196 197 gtr_dialog_set_content( GTK_DIALOG( d ), fr ); 198 gtk_widget_show( d ); 199} 200 201static void 202onResponse( GtkDialog* d, int response, gpointer user_data ) 203{ 204 MakeMetaUI * ui = user_data; 205 206 if( response == GTK_RESPONSE_ACCEPT ) 207 { 208 if( ui->builder != NULL ) 209 { 210 int i; 211 int n; 212 int tier; 213 GtkTextIter start, end; 214 char * dir; 215 char * base; 216 char * tracker_text; 217 char ** tracker_strings; 218 GtkEntry * c_entry = GTK_ENTRY( ui->comment_entry ); 219 GtkToggleButton * p_check = GTK_TOGGLE_BUTTON( ui->private_check ); 220 GtkToggleButton * c_check = GTK_TOGGLE_BUTTON( ui->comment_check ); 221 const char * comment = gtk_entry_get_text( c_entry ); 222 const gboolean isPrivate = gtk_toggle_button_get_active( p_check ); 223 const gboolean useComment = gtk_toggle_button_get_active( c_check ); 224 tr_tracker_info * trackers; 225 226 /* destination file */ 227 dir = gtk_file_chooser_get_filename( 228 GTK_FILE_CHOOSER( ui->destination_chooser ) ); 229 base = g_path_get_basename( ui->builder->top ); 230 g_free( ui->target ); 231 ui->target = g_strdup_printf( "%s/%s.torrent", dir, base ); 232 233 /* build the array of trackers */ 234 gtk_text_buffer_get_bounds( ui->announce_text_buffer, &start, &end ); 235 tracker_text = gtk_text_buffer_get_text( ui->announce_text_buffer, 236 &start, &end, FALSE ); 237 tracker_strings = g_strsplit( tracker_text, "\n", 0 ); 238 for( i=0; tracker_strings[i]; ) 239 ++i; 240 trackers = g_new0( tr_tracker_info, i ); 241 for( i=n=tier=0; tracker_strings[i]; ++i ) { 242 const char * str = tracker_strings[i]; 243 if( !*str ) 244 ++tier; 245 else { 246 trackers[n].tier = tier; 247 trackers[n].announce = tracker_strings[i]; 248 ++n; 249 } 250 } 251 252 /* build the .torrent */ 253 makeProgressDialog( GTK_WIDGET( d ), ui ); 254 tr_makeMetaInfo( ui->builder, ui->target, trackers, n, 255 useComment ? comment : NULL, isPrivate ); 256 257 /* cleanup */ 258 g_free( trackers ); 259 g_strfreev( tracker_strings ); 260 g_free( tracker_text ); 261 g_free( base ); 262 g_free( dir ); 263 } 264 } 265 else if( response == GTK_RESPONSE_CLOSE ) 266 { 267 gtk_widget_destroy( GTK_WIDGET( d ) ); 268 } 269} 270 271/*** 272**** 273***/ 274 275static void 276onSourceToggled( GtkToggleButton * tb, gpointer user_data ) 277{ 278 gtk_widget_set_sensitive( GTK_WIDGET( user_data ), 279 gtk_toggle_button_get_active( tb ) ); 280} 281 282static void 283updatePiecesLabel( MakeMetaUI * ui ) 284{ 285 const tr_metainfo_builder * builder = ui->builder; 286 const char * filename = builder ? builder->top : NULL; 287 GString * gstr = g_string_new( NULL ); 288 289 g_string_append( gstr, "<i>" ); 290 if( !filename ) 291 { 292 g_string_append( gstr, _( "No source selected" ) ); 293 } 294 else 295 { 296 char buf[128]; 297 tr_strlsize( buf, builder->totalSize, sizeof( buf ) ); 298 g_string_append_printf( gstr, ngettext( "%1$s; %2$'d File", 299 "%1$s; %2$'d Files", 300 builder->fileCount ), 301 buf, builder->fileCount ); 302 g_string_append( gstr, "; " ); 303 304 tr_formatter_mem_B( buf, builder->pieceSize, sizeof( buf ) ); 305 g_string_append_printf( gstr, ngettext( "%1$'d Piece @ %2$s", 306 "%1$'d Pieces @ %2$s", 307 builder->pieceCount ), 308 builder->pieceCount, buf ); 309 } 310 g_string_append( gstr, "</i>" ); 311 gtk_label_set_markup ( GTK_LABEL( ui->pieces_lb ), gstr->str ); 312 g_string_free( gstr, TRUE ); 313} 314 315static void 316setFilename( MakeMetaUI * ui, const char * filename ) 317{ 318 if( ui->builder ) { 319 tr_metaInfoBuilderFree( ui->builder ); 320 ui->builder = NULL; 321 } 322 323 if( filename ) 324 ui->builder = tr_metaInfoBuilderCreate( filename ); 325 326 updatePiecesLabel( ui ); 327} 328 329static void 330onChooserChosen( GtkFileChooser * chooser, gpointer user_data ) 331{ 332 char * filename; 333 MakeMetaUI * ui = user_data; 334 335 g_object_set_data( G_OBJECT( chooser ), FILE_CHOSEN_KEY, 336 GINT_TO_POINTER( TRUE ) ); 337 338 filename = gtk_file_chooser_get_filename( chooser ); 339 setFilename( ui, filename ); 340 g_free( filename ); 341} 342 343static void 344onSourceToggled2( GtkToggleButton * tb, GtkWidget * chooser, MakeMetaUI * ui ) 345{ 346 if( gtk_toggle_button_get_active( tb ) ) 347 { 348 if( g_object_get_data( G_OBJECT( chooser ), FILE_CHOSEN_KEY ) != NULL ) 349 onChooserChosen( GTK_FILE_CHOOSER( chooser ), ui ); 350 else 351 setFilename( ui, NULL ); 352 } 353} 354static void 355onFolderToggled( GtkToggleButton * tb, gpointer data ) 356{ 357 MakeMetaUI * ui = data; 358 onSourceToggled2( tb, ui->folder_chooser, ui ); 359} 360static void 361onFileToggled( GtkToggleButton * tb, gpointer data ) 362{ 363 MakeMetaUI * ui = data; 364 onSourceToggled2( tb, ui->file_chooser, ui ); 365} 366 367static const char * 368getDefaultSavePath( void ) 369{ 370 return g_get_user_special_dir( G_USER_DIRECTORY_DESKTOP ); 371} 372 373static void 374on_drag_data_received( GtkWidget * widget UNUSED, 375 GdkDragContext * drag_context, 376 gint x UNUSED, 377 gint y UNUSED, 378 GtkSelectionData * selection_data, 379 guint info UNUSED, 380 guint time_, 381 gpointer user_data ) 382{ 383 gboolean success = FALSE; 384 MakeMetaUI * ui = user_data; 385 char ** uris = gtk_selection_data_get_uris( selection_data ); 386 387 if( uris && uris[0] ) 388 { 389 const char * uri = uris[ 0 ]; 390 gchar * filename = g_filename_from_uri( uri, NULL, NULL ); 391 392 if( g_file_test( filename, G_FILE_TEST_IS_DIR ) ) 393 { 394 /* a directory was dragged onto the dialog... */ 395 gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( ui->folder_radio ), TRUE ); 396 gtk_file_chooser_set_current_folder( GTK_FILE_CHOOSER( ui->folder_chooser ), filename ); 397 success = TRUE; 398 } 399 else if( g_file_test( filename, G_FILE_TEST_IS_REGULAR ) ) 400 { 401 /* a file was dragged on to the dialog... */ 402 gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( ui->file_radio ), TRUE ); 403 gtk_file_chooser_set_filename( GTK_FILE_CHOOSER( ui->file_chooser ), filename ); 404 success = TRUE; 405 } 406 407 g_free( filename ); 408 } 409 410 g_strfreev( uris ); 411 gtk_drag_finish( drag_context, success, FALSE, time_ ); 412} 413 414GtkWidget* 415gtr_torrent_creation_dialog_new( GtkWindow * parent, TrCore * core ) 416{ 417 const char * str; 418 GtkWidget * d, *t, *w, *l, *fr, *sw, *v; 419 GSList * slist; 420 guint row = 0; 421 MakeMetaUI * ui = g_new0 ( MakeMetaUI, 1 ); 422 423 ui->core = core; 424 425 d = gtk_dialog_new_with_buttons( _( "New Torrent" ), 426 parent, 427 GTK_DIALOG_DESTROY_WITH_PARENT, 428 GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, 429 GTK_STOCK_NEW, GTK_RESPONSE_ACCEPT, 430 NULL ); 431 ui->dialog = d; 432 g_signal_connect( d, "response", G_CALLBACK( onResponse ), ui ); 433 g_object_set_data_full( G_OBJECT( d ), "ui", ui, freeMetaUI ); 434 435 t = hig_workarea_create ( ); 436 437 hig_workarea_add_section_title ( t, &row, _( "Files" ) ); 438 439 str = _( "Sa_ve to:" ); 440 w = gtk_file_chooser_button_new( NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER ); 441 gtk_file_chooser_set_current_folder( GTK_FILE_CHOOSER( w ), getDefaultSavePath( ) ); 442 ui->destination_chooser = w; 443 hig_workarea_add_row( t, &row, str, w, NULL ); 444 445 l = gtk_radio_button_new_with_mnemonic( NULL, _( "Source F_older:" ) ); 446 gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( l ), FALSE ); 447 w = gtk_file_chooser_button_new( NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER ); 448 g_signal_connect( l, "toggled", G_CALLBACK( onFolderToggled ), ui ); 449 g_signal_connect( l, "toggled", G_CALLBACK( onSourceToggled ), w ); 450 g_signal_connect( w, "selection-changed", G_CALLBACK( onChooserChosen ), ui ); 451 ui->folder_radio = l; 452 ui->folder_chooser = w; 453 gtk_widget_set_sensitive( GTK_WIDGET( w ), FALSE ); 454 hig_workarea_add_row_w( t, &row, l, w, NULL ); 455 456 slist = gtk_radio_button_get_group( GTK_RADIO_BUTTON( l ) ), 457 l = gtk_radio_button_new_with_mnemonic( slist, _( "Source _File:" ) ); 458 gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( l ), TRUE ); 459 w = gtk_file_chooser_button_new( NULL, GTK_FILE_CHOOSER_ACTION_OPEN ); 460 g_signal_connect( l, "toggled", G_CALLBACK( onFileToggled ), ui ); 461 g_signal_connect( l, "toggled", G_CALLBACK( onSourceToggled ), w ); 462 g_signal_connect( w, "selection-changed", G_CALLBACK( onChooserChosen ), ui ); 463 ui->file_radio = l; 464 ui->file_chooser = w; 465 hig_workarea_add_row_w( t, &row, l, w, NULL ); 466 467 w = gtk_label_new( NULL ); 468 ui->pieces_lb = w; 469 gtk_label_set_markup( GTK_LABEL( w ), _( "<i>No source selected</i>" ) ); 470 hig_workarea_add_row( t, &row, NULL, w, NULL ); 471 472 hig_workarea_add_section_divider( t, &row ); 473 hig_workarea_add_section_title ( t, &row, _( "Properties" ) ); 474 475 str = _( "_Trackers:" ); 476 v = gtk_box_new( GTK_ORIENTATION_VERTICAL, GUI_PAD_SMALL ); 477 ui->announce_text_buffer = gtk_text_buffer_new( NULL ); 478 w = gtk_text_view_new_with_buffer( ui->announce_text_buffer ); 479 gtk_widget_set_size_request( w, -1, 80 ); 480 sw = gtk_scrolled_window_new( NULL, NULL ); 481 gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( sw ), 482 GTK_POLICY_AUTOMATIC, 483 GTK_POLICY_AUTOMATIC ); 484 gtk_container_add( GTK_CONTAINER( sw ), w ); 485 fr = gtk_frame_new( NULL ); 486 gtk_frame_set_shadow_type( GTK_FRAME( fr ), GTK_SHADOW_IN ); 487 gtk_container_add( GTK_CONTAINER( fr ), sw ); 488 gtk_box_pack_start( GTK_BOX( v ), fr, TRUE, TRUE, 0 ); 489 l = gtk_label_new( NULL ); 490 gtk_label_set_markup( GTK_LABEL( l ), _( "To add a backup URL, add it on the line after the primary URL.\n" 491 "To add another primary URL, add it after a blank line." ) ); 492 gtk_label_set_justify( GTK_LABEL( l ), GTK_JUSTIFY_LEFT ); 493 gtk_misc_set_alignment( GTK_MISC( l ), 0.0, 0.5 ); 494 gtk_box_pack_start( GTK_BOX( v ), l, FALSE, FALSE, 0 ); 495 hig_workarea_add_tall_row( t, &row, str, v, NULL ); 496 497 l = gtk_check_button_new_with_mnemonic( _( "Co_mment:" ) ); 498 ui->comment_check = l; 499 gtk_toggle_button_set_active( GTK_TOGGLE_BUTTON( l ), FALSE ); 500 w = gtk_entry_new( ); 501 ui->comment_entry = w; 502 gtk_widget_set_sensitive( GTK_WIDGET( w ), FALSE ); 503 g_signal_connect( l, "toggled", G_CALLBACK( onSourceToggled ), w ); 504 hig_workarea_add_row_w( t, &row, l, w, NULL ); 505 506 w = hig_workarea_add_wide_checkbutton( t, &row, _( "_Private torrent" ), FALSE ); 507 ui->private_check = w; 508 509 gtr_dialog_set_content( GTK_DIALOG( d ), t ); 510 511 gtk_drag_dest_set( d, GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY ); 512 gtk_drag_dest_add_uri_targets( d ); 513 g_signal_connect( d, "drag-data-received", G_CALLBACK( on_drag_data_received ), ui ); 514 515 return d; 516} 517