/****************************************************************************** * $Id$ * * Copyright (c) 2005-2007 Transmission authors and contributors * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. *****************************************************************************/ #include #include #include #include "transmission.h" #include "tr_cell_renderer_progress.h" #include "tr_core.h" #include "tr_torrent.h" #include "tr_window.h" #include "util.h" #define ITEM_ACTION "tr-window-item-action" enum { PROP_MODEL = 1, PROP_SELECTION, PROP_DOUBLECLICK, PROP_DRAG, }; static void tr_window_init( GTypeInstance * instance, gpointer g_class ); static void tr_window_set_property( GObject * object, guint property_id, const GValue * value, GParamSpec * pspec ); static void tr_window_get_property( GObject * object, guint property_id, GValue * value, GParamSpec * pspec); static void tr_window_class_init( gpointer g_class, gpointer g_class_data ); static void tr_window_dispose( GObject * obj ); static GtkTreeView * makeview( TrWindow * self ); static void stylekludge( GObject * obj, GParamSpec * spec, gpointer data ); static void fixbuttons( GtkTreeSelection *sel, TrWindow * self ); static void formatname( GtkTreeViewColumn * col, GtkCellRenderer * rend, GtkTreeModel * model, GtkTreeIter * iter, gpointer data ); static void formatprog( GtkTreeViewColumn * col, GtkCellRenderer * rend, GtkTreeModel * model, GtkTreeIter * iter, gpointer data ); static gboolean listclick( GtkWidget * view, GdkEventButton * event, gpointer data ); static gboolean listpopup( GtkWidget * view SHUTUP, gpointer data ); static void popupmenu( TrWindow * self, GdkEventButton * event ); static void itemclick( GObject * obj, gpointer data ); static void doubleclick( GtkWidget * view, GtkTreePath * path, GtkTreeViewColumn * col SHUTUP, gpointer data ); static void emitaction( TrWindow * self, int id ); static void orstatus( GtkTreeModel * model, GtkTreePath * path SHUTUP, GtkTreeIter * iter, gpointer data ); static void istorsel( GtkTreeModel * model, GtkTreePath * path SHUTUP, GtkTreeIter * iter, gpointer data ); GType tr_window_get_type( void ) { static GType type = 0; if( 0 == type ) { static const GTypeInfo info = { sizeof( TrWindowClass ), NULL, /* base_init */ NULL, /* base_finalize */ tr_window_class_init, /* class_init */ NULL, /* class_finalize */ NULL, /* class_data */ sizeof( TrWindow ), 0, /* n_preallocs */ tr_window_init, /* instance_init */ NULL, }; type = g_type_register_static( GTK_TYPE_WINDOW, "TrWindow", &info, 0 ); } return type; } static void tr_window_class_init( gpointer g_class, gpointer g_class_data SHUTUP ) { GObjectClass * gobject_class; TrWindowClass * trwindow_class; GParamSpec * pspec; gobject_class = G_OBJECT_CLASS( g_class ); gobject_class->set_property = tr_window_set_property; gobject_class->get_property = tr_window_get_property; gobject_class->dispose = tr_window_dispose; pspec = g_param_spec_object( "model", "Model", "The GtkTreeModel for the list view.", GTK_TYPE_TREE_MODEL, G_PARAM_READWRITE ); g_object_class_install_property( gobject_class, PROP_MODEL, pspec ); pspec = g_param_spec_object( "selection", "Selection", "The GtkTreeSelection for the list view.", GTK_TYPE_TREE_SELECTION, G_PARAM_READABLE ); g_object_class_install_property( gobject_class, PROP_SELECTION, pspec ); pspec = g_param_spec_int( "double-click-action", "Double-click action", "The action id to signal on a double click.", G_MININT, G_MAXINT, -1, G_PARAM_READWRITE ); g_object_class_install_property( gobject_class, PROP_DOUBLECLICK, pspec ); pspec = g_param_spec_object( "drag-widget", "Drag widget", "The GtkWidget used for drag-and-drop.", GTK_TYPE_WIDGET, G_PARAM_READABLE ); g_object_class_install_property( gobject_class, PROP_DRAG, pspec ); trwindow_class = TR_WINDOW_CLASS( g_class ); trwindow_class->actionsig = g_signal_new( "action", G_TYPE_FROM_CLASS( g_class ), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__INT, G_TYPE_NONE, 1, G_TYPE_INT ); } static void tr_window_init( GTypeInstance * instance, gpointer g_class SHUTUP ) { TrWindow * self = ( TrWindow * )instance; GtkWidget * vbox, * scroll, * status, * tools, * menu, * file, * item; vbox = gtk_vbox_new( FALSE, 0 ); scroll = gtk_scrolled_window_new( NULL, NULL ); status = gtk_statusbar_new(); tools = gtk_toolbar_new(); menu = gtk_menu_bar_new(); file = gtk_menu_new(); item = gtk_menu_item_new_with_mnemonic( _("_File") ); self->scroll = GTK_SCROLLED_WINDOW( scroll ); self->view = makeview( self ); self->status = GTK_STATUSBAR( status ); self->toolbar = GTK_TOOLBAR( tools ); self->menu = GTK_MENU_SHELL( file ); /* this should have been set by makeview() */ g_assert( NULL != self->namerend ); self->doubleclick = -1; self->actions = NULL; self->accel = gtk_accel_group_new(); self->stupidpopuphack = NULL; self->disposed = FALSE; gtk_window_add_accel_group( GTK_WINDOW( self ), self->accel ); gtk_menu_set_accel_group( GTK_MENU( self->menu ), self->accel ); gtk_menu_item_set_submenu( GTK_MENU_ITEM( item ), file ); gtk_menu_shell_append( GTK_MENU_SHELL( menu ), item ); gtk_box_pack_start( GTK_BOX( vbox ), menu, FALSE, FALSE, 0 ); gtk_toolbar_set_tooltips( self->toolbar, TRUE ); gtk_toolbar_set_show_arrow( self->toolbar, FALSE ); gtk_box_pack_start( GTK_BOX( vbox ), GTK_WIDGET( self->toolbar ), FALSE, FALSE, 0 ); gtk_container_add( GTK_CONTAINER( scroll ), GTK_WIDGET( self->view ) ); gtk_box_pack_start( GTK_BOX( vbox ), scroll, TRUE, TRUE, 0 ); gtk_statusbar_push( self->status, 0, "" ); gtk_box_pack_start( GTK_BOX( vbox ), GTK_WIDGET( self->status ), FALSE, FALSE, 0 ); gtk_container_set_focus_child( GTK_CONTAINER( vbox ), scroll ); gtk_widget_show_all( vbox ); gtk_container_add( GTK_CONTAINER( self ), vbox ); gtk_window_set_title( GTK_WINDOW( self ), g_get_application_name()); gtk_window_set_role( GTK_WINDOW( self ), "tr-main" ); } static void tr_window_set_property( GObject * object, guint property_id, const GValue * value SHUTUP, GParamSpec * pspec) { TrWindow * self = ( TrWindow * )object; if( self->disposed ) { return; } switch( property_id ) { case PROP_MODEL: gtk_tree_view_set_model( self->view, g_value_get_object( value ) ); break; case PROP_DOUBLECLICK: self->doubleclick = g_value_get_int( value ); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID( object, property_id, pspec ); break; } } static void tr_window_get_property( GObject * object, guint property_id, GValue * value SHUTUP, GParamSpec * pspec ) { TrWindow * self = ( TrWindow * )object; if( self->disposed ) { return; } switch( property_id ) { case PROP_MODEL: g_value_set_object( value, gtk_tree_view_get_model( self->view ) ); break; case PROP_SELECTION: g_value_set_object( value, gtk_tree_view_get_selection( self->view ) ); break; case PROP_DOUBLECLICK: g_value_set_int( value, self->doubleclick ); break; case PROP_DRAG: g_value_set_object( value, self->view ); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID( object, property_id, pspec ); break; } } static void tr_window_dispose( GObject * obj ) { TrWindow * self = ( TrWindow * )obj; GObjectClass * parent; if( self->disposed ) { return; } self->disposed = TRUE; g_list_foreach( self->actions, ( GFunc )action_free, NULL ); g_list_free( self->actions ); g_object_unref( self->accel ); if( NULL != self->stupidpopuphack ) { gtk_widget_destroy( self->stupidpopuphack ); } self->stupidpopuphack = NULL; /* Chain up to the parent class */ parent = g_type_class_peek( g_type_parent( TR_WINDOW_TYPE ) ); parent->dispose( obj ); } GtkWidget * tr_window_new( void ) { return g_object_new( TR_WINDOW_TYPE, NULL ); } void tr_window_action_add( TrWindow * self, int id, int flags, const char * name, const char * icon, const char * description, guint key ) { struct action * act; GtkWidget * sep; TR_IS_WINDOW( self ); if( self->disposed ) { return; } act = action_new( id, flags, name, icon ); if( ACTF_TOOL & flags ) { act->tool = action_maketool( act, ITEM_ACTION, G_CALLBACK( itemclick ), self ); gtk_tool_item_set_tooltip( GTK_TOOL_ITEM( act->tool ), self->toolbar->tooltips, description, "" ); gtk_toolbar_insert( self->toolbar, GTK_TOOL_ITEM( act->tool ), -1 ); } if( ACTF_MENU & flags ) { act->menu = action_makemenu( act, ITEM_ACTION, self->accel, "/file", key, G_CALLBACK( itemclick ), self ); gtk_menu_shell_append( self->menu, act->menu ); } if( ACTF_SEPARATOR & flags ) { sep = gtk_separator_menu_item_new(); gtk_widget_show( sep ); gtk_menu_shell_append( self->menu, sep ); } self->actions = g_list_append( self->actions, act ); } void tr_window_update( TrWindow * self, float downspeed, float upspeed ) { char * downstr, * upstr, * str; TR_IS_WINDOW( self ); if( self->disposed ) { return; } /* update the status bar */ downstr = readablesize( downspeed * 1024.0 ); upstr = readablesize( upspeed * 1024.0 ); str = g_strdup_printf( _(" Total DL: %s/s Total UL: %s/s"), downstr, upstr ); g_free( downstr ); g_free( upstr ); gtk_statusbar_pop( self->status, 0 ); gtk_statusbar_push( self->status, 0, str ); g_free( str ); /* the selection's status may have changed so update the buttons */ fixbuttons( NULL, self ); } void tr_window_show( TrWindow * self ) { TR_IS_WINDOW( self ); sizingmagic( GTK_WINDOW( self ), self->scroll, GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS ); g_object_set( self->namerend, "ellipsize", PANGO_ELLIPSIZE_END, NULL ); gtk_widget_show( GTK_WIDGET( self ) ); } static GtkTreeView * makeview( TrWindow * self ) { GtkWidget * view; GtkTreeViewColumn * col; GtkTreeSelection * sel; GtkCellRenderer * namerend, * progrend; char * str; TR_IS_WINDOW( self ); view = gtk_tree_view_new(); namerend = gtk_cell_renderer_text_new(); self->namerend = G_OBJECT( namerend ); /* note that this renderer is set to ellipsize, just not here */ col = gtk_tree_view_column_new_with_attributes( _("Name"), namerend, NULL ); gtk_tree_view_column_set_cell_data_func( col, namerend, formatname, NULL, NULL ); gtk_tree_view_column_set_expand( col, TRUE ); gtk_tree_view_column_set_sizing( col, GTK_TREE_VIEW_COLUMN_AUTOSIZE ); gtk_tree_view_append_column( GTK_TREE_VIEW( view ), col ); progrend = tr_cell_renderer_progress_new(); /* this string is only used to determine the size of the progress bar */ str = g_markup_printf_escaped( "%s", _(" fnord fnord ") ); g_object_set( progrend, "bar-sizing", str, NULL ); g_free(str); col = gtk_tree_view_column_new_with_attributes( _("Progress"), progrend, NULL); gtk_tree_view_column_set_cell_data_func( col, progrend, formatprog, NULL, NULL ); gtk_tree_view_column_set_sizing( col, GTK_TREE_VIEW_COLUMN_AUTOSIZE ); gtk_tree_view_append_column( GTK_TREE_VIEW( view ), col ); /* XXX this shouldn't be necessary */ g_signal_connect( view, "notify::style", G_CALLBACK( stylekludge ), progrend ); gtk_tree_view_set_rules_hint( GTK_TREE_VIEW( view ), TRUE ); sel = gtk_tree_view_get_selection( GTK_TREE_VIEW( view ) ); gtk_tree_selection_set_mode( GTK_TREE_SELECTION( sel ), GTK_SELECTION_MULTIPLE ); g_signal_connect( G_OBJECT( sel ), "changed", G_CALLBACK( fixbuttons ), self ); g_signal_connect( G_OBJECT( view ), "button-press-event", G_CALLBACK( listclick ), self ); g_signal_connect( G_OBJECT( view ), "popup-menu", G_CALLBACK( listpopup ), self ); g_signal_connect( G_OBJECT( view ), "row-activated", G_CALLBACK( doubleclick ), self ); return GTK_TREE_VIEW( view ); } /* kludge to have the progress bars notice theme changes */ static void stylekludge( GObject * obj, GParamSpec * spec, gpointer data ) { if( 0 == strcmp( "style", spec->name ) ) { tr_cell_renderer_progress_reset_style( TR_CELL_RENDERER_PROGRESS( data ) ); gtk_widget_queue_draw( GTK_WIDGET( obj ) ); } } /* disable buttons and menuitems the user shouldn't be able to click on */ static void fixbuttons( GtkTreeSelection *sel, TrWindow * self ) { gboolean selected, avail; GList * ii; int status; struct action * act; TR_IS_WINDOW( self ); if( self->disposed ) { return; } if( NULL == sel ) { sel = gtk_tree_view_get_selection( self->view ); } status = 0; gtk_tree_selection_selected_foreach( sel, orstatus, &status ); selected = ( 0 < gtk_tree_selection_count_selected_rows( sel ) ); for( ii = g_list_first( self->actions ); NULL != ii; ii = ii->next ) { act = ii->data; if( ACTF_ALWAYS & act->flags ) { continue; } avail = ACT_ISAVAIL( act->flags, status ); if( ACTF_TOOL & act->flags ) { g_assert( NULL != act->tool ); gtk_widget_set_sensitive( act->tool, selected && avail ); } if( ACTF_MENU & act->flags ) { g_assert( NULL != act->menu ); gtk_widget_set_sensitive( act->menu, selected && avail ); } } } static void formatname( GtkTreeViewColumn * col SHUTUP, GtkCellRenderer * rend, GtkTreeModel * model, GtkTreeIter * iter, gpointer data SHUTUP ) { char * name, * mb, * terr, * str, * top, * bottom, * timestr; guint64 size; gfloat prog; int status, err, eta, tpeers, upeers, dpeers; gtk_tree_model_get( model, iter, MC_NAME, &name, MC_STAT, &status, MC_ERR, &err, MC_SIZE, &size, MC_PROG, &prog, MC_ETA, &eta, MC_PEERS, &tpeers, MC_UPEERS, &upeers, MC_DPEERS, &dpeers, -1 ); tpeers = MAX( tpeers, 0 ); upeers = MAX( upeers, 0 ); dpeers = MAX( dpeers, 0 ); mb = readablesize(size); prog *= 100; if( TR_STATUS_CHECK_WAIT & status ) { top = g_strdup_printf( _("Waiting to check existing files (%.1f%%)"), prog ); } else if( TR_STATUS_CHECK & status ) { top = g_strdup_printf( _("Checking existing files (%.1f%%)"), prog ); } else if( TR_STATUS_DOWNLOAD & status ) { if( 0 > eta ) { top = g_strdup_printf( _("Stalled (%.1f%%)"), prog ); } else { timestr = readabletime(eta); top = g_strdup_printf( _("Finishing in %s (%.1f%%)"), timestr, prog ); g_free(timestr); } } else if(TR_STATUS_SEED & status) { top = g_strdup_printf( ngettext( "Seeding, uploading to %d of %d peer", "Seeding, uploading to %d of %d peers", tpeers ), dpeers, tpeers ); } else if( TR_STATUS_STOPPING & status ) { top = g_strdup( _("Stopping...") ); } else if( TR_STATUS_PAUSE & status ) { top = g_strdup_printf( _("Stopped (%.1f%%)"), prog ); } else { top = g_strdup( "" ); g_assert_not_reached(); } if( TR_OK != err ) { gtk_tree_model_get( model, iter, MC_TERR, &terr, -1 ); bottom = g_strconcat( _("Error: "), terr, NULL ); g_free( terr ); } else if( TR_STATUS_DOWNLOAD & status ) { bottom = g_strdup_printf( ngettext( "Downloading from %i of %i peer", "Downloading from %i of %i peers", tpeers ), upeers, tpeers ); } else { bottom = NULL; } str = g_markup_printf_escaped( "%s (%s)\n%s\n%s", name, mb, top, ( NULL == bottom ? "" : bottom ) ); g_object_set( rend, "markup", str, NULL ); g_free( name ); g_free( mb ); g_free( str ); g_free( top ); g_free( bottom ); } static void formatprog( GtkTreeViewColumn * col SHUTUP, GtkCellRenderer * rend, GtkTreeModel * model, GtkTreeIter * iter, gpointer data SHUTUP ) { char * dlstr, * ulstr, * str, * marked; gfloat prog, dl, ul; guint64 down, up; gtk_tree_model_get( model, iter, MC_PROG, &prog, MC_DRATE, &dl, MC_URATE, &ul, MC_DOWN, &down, MC_UP, &up, -1 ); prog = MAX( prog, 0.0 ); prog = MIN( prog, 1.0 ); ulstr = readablesize( ul * 1024.0 ); if( 1.0 == prog ) { dlstr = ratiostr( down, up ); str = g_strdup_printf( _("Ratio: %s\nUL: %s/s"), dlstr, ulstr ); } else { dlstr = readablesize( dl * 1024.0 ); str = g_strdup_printf( _("DL: %s/s\nUL: %s/s"), dlstr, ulstr ); } marked = g_markup_printf_escaped( "%s", str ); g_object_set( rend, "markup", str, "progress", prog, NULL ); g_free( dlstr ); g_free( ulstr ); g_free( str ); g_free( marked ); } /* show a popup menu for a right-click on the list */ static gboolean listclick( GtkWidget * view, GdkEventButton * event, gpointer data ) { TrWindow * self; GtkTreeSelection * sel; GtkTreePath * path; GtkTreeModel * model; GtkTreeIter iter; int status; TrTorrent * tor, * issel; if( GDK_BUTTON_PRESS != event->type || 3 != event->button ) { return FALSE; } TR_IS_WINDOW( data ); self = TR_WINDOW( data ); if( self->disposed ) { return FALSE; } sel = gtk_tree_view_get_selection( GTK_TREE_VIEW( view ) ); model = gtk_tree_view_get_model( GTK_TREE_VIEW( view ) ); /* find what row, if any, the user clicked on */ if( gtk_tree_view_get_path_at_pos( GTK_TREE_VIEW( view ), event->x, event->y, &path, NULL, NULL, NULL ) ) { if( gtk_tree_model_get_iter( model, &iter, path ) ) { /* get torrent and status for the right-clicked row */ gtk_tree_model_get( model, &iter, MC_TORRENT, &tor, MC_STAT, &status, -1 ); issel = tor; gtk_tree_selection_selected_foreach( sel, istorsel, &issel ); g_object_unref( tor ); /* if the clicked row isn't selected, select only it */ if( NULL != issel ) { gtk_tree_selection_unselect_all( sel ); gtk_tree_selection_select_iter( sel, &iter ); } } gtk_tree_path_free( path ); } else { gtk_tree_selection_unselect_all( sel ); } popupmenu( self, event ); return TRUE; } static gboolean listpopup( GtkWidget * view SHUTUP, gpointer data ) { popupmenu( TR_WINDOW( data ), NULL ); return TRUE; } static void popupmenu( TrWindow * self, GdkEventButton * event ) { GtkTreeSelection * sel; int count, status; GtkWidget * menu, * item; GList * ii; struct action * act; TR_IS_WINDOW( self ); if( self->disposed ) { return; } sel = gtk_tree_view_get_selection( self->view ); count = gtk_tree_selection_count_selected_rows( sel ); menu = gtk_menu_new(); if( NULL != self->stupidpopuphack ) { gtk_widget_destroy( self->stupidpopuphack ); } self->stupidpopuphack = menu; status = 0; gtk_tree_selection_selected_foreach( sel, orstatus, &status ); for( ii = g_list_first( self->actions ); NULL != ii; ii = ii->next ) { act = ii->data; if( ACTF_SEPARATOR & act->flags ) { item = gtk_separator_menu_item_new(); gtk_widget_show( item ); gtk_menu_shell_append( GTK_MENU_SHELL( menu ), item ); } else if( ACTF_MENU & act->flags && ACT_ISAVAIL( act->flags, status ) ) { item = action_makemenu( act, ITEM_ACTION, NULL, NULL, 0, G_CALLBACK( itemclick ), self ); gtk_menu_shell_append( GTK_MENU_SHELL( menu ), item ); } } gtk_widget_show( menu ); gtk_menu_popup( GTK_MENU( menu ), NULL, NULL, NULL, NULL, ( NULL == event ? 0 : event->button ), gdk_event_get_time( (GdkEvent*)event ) ); } static void itemclick( GObject * obj, gpointer data ) { TrWindow * self; struct action * act; TR_IS_WINDOW( data ); self = TR_WINDOW( data ); act = g_object_get_data( obj, ITEM_ACTION ); emitaction( self, act->id ); } static void doubleclick( GtkWidget * view SHUTUP, GtkTreePath * path, GtkTreeViewColumn * col SHUTUP, gpointer data ) { TrWindow * self; GtkTreeSelection * sel; TR_IS_WINDOW( data ); self = TR_WINDOW( data ); if( self->disposed || 0 > self->doubleclick ) { return; } sel = gtk_tree_view_get_selection( self->view ); gtk_tree_selection_select_path( sel, path ); emitaction( self, self->doubleclick ); } static void emitaction( TrWindow * self, int id ) { TrWindowClass * class; TR_IS_WINDOW( self ); if( self->disposed ) { return; } class = g_type_class_peek( TR_WINDOW_TYPE ); g_signal_emit( self, class->actionsig, 0, id ); } /* use with gtk_tree_selection_selected_foreach to | status of selected rows */ static void orstatus( GtkTreeModel * model, GtkTreePath * path SHUTUP, GtkTreeIter * iter, gpointer data ) { int * allstatus, thisstatus; allstatus = data; gtk_tree_model_get( model, iter, MC_STAT, &thisstatus, -1 ); *allstatus |= thisstatus; } /* data should be a TrTorrent**, will set torrent to NULL if it's selected */ static void istorsel( GtkTreeModel * model, GtkTreePath * path SHUTUP, GtkTreeIter * iter, gpointer data ) { TrTorrent ** torref, * tor; torref = data; if( NULL != *torref ) { gtk_tree_model_get( model, iter, MC_TORRENT, &tor, -1 ); if( tor == *torref ) { *torref = NULL; } g_object_unref( tor ); } }