/* * This is a Gtk2 replacement for the MP3FileManager program shipped * with the Sony NW-S23. It currently needs the following things * fixed (X marks the ones I've fixed): * * X dropping folders at specific positions doesn't work; they always * get appended to the end of the folder list. You can move 'em * around once they're on the device, though. * X changes are synced to the device far too often, and the syncing * is a huge kludge that involves deleting the entire track/folder * list, syncing that, then rebuilding the new list from * scratch. Mostly this is because I need to add a few more hooks to * mple.c * X doesn't delete mp*.dat files when tracks are deleted from the * UI. The index entries *are* deleted, so the mpdat files won't * play, though. * * Delete All button does nothing * * You can't select more than one thing at a time from the * list. This is a serious pain if you're trying to make bulk * changes, such as moving a half-dozen tracks. This can't be fixed * without subclassing, see below. * * Delete All and Delete both have confirm dialogs in the Windows * version. * * The file transfer stuff needs a little fixing - the main window * should be disabled; this is done with - I think - an * application-modal window on Windows. A better idea would be to * thread the transfer process so you could keep adding files while * the current transfer is in progress. * * You can't drop a deep directory structure on it and expect it to * do something useful. Windows will collapse a Band/Album/Mp3.mp3 * filename into Folder "Band Album", Track Mp3.mp3 * X Files should be listed by the mp3 filename, not the mp3 title. * * The windows version doesn't use a progress bar for * transfers. While I like the progress bar, it does mean that the * status bar is only half as wide as it should be. * * It would be nice if I could handle any URI that drag-and-drop * throws me; at the very least, dragging a URL from firefox into * the gadget would be cool. * * Context menu is broken * * Context menu should allow you to edit the metadata of a file * X The device path is hardcoded. * * Mount/Unmount device would be neat * * dropping is still not quite right * - dropping a file at the end of the device * - dropping a file onto a folder prepends instead of appends * * And internally; * * the code is full of memory leaks. I did a dry-run with valgrind, * but there's so much noise from Gtk and X that I gave up. * * Globals. I hate globals. I hate hate hate hate them. * * Might be smart to isolate the unix-specific stuff if I'm going to * try building this on Windows at some point. There are probably * glib functions to help me. * * The code is, in general, pretty horrible, in part because I'm * trying to force the default GtkTree stuff to do things it's not * really meant to do. At some point I need to learn how to subclass * the GtkTreeStore so I can add on the bits I need, such as better * drag/drop, edit on button-click rather than button-press, etc. * * The other reason the code is horrible is that I've not done any * serious Gtk work before, and it's not exactly intuitive. I keep * finding useful glib functions after I've spent half an hour writing * equivalent code, too. Henry Spencer would be proud... */ /* Sigh. "GTK+ supports Drag-and-Drop in tree views with a high-level and a low-level API. The low-level API consists of the GTK+ DND API, augmented by some treeview utility functions: gtk_tree_view_set_drag_dest_row(), gtk_tree_view_get_drag_dest_row(), gtk_tree_view_get_dest_row_at_pos(), gtk_tree_view_create_row_drag_icon(), gtk_tree_set_row_drag_data() and gtk_tree_get_row_drag_data(). This API leaves a lot of flexibility, but nothing is done automatically, and implementing advanced features like hover-to-open-rows or autoscrolling on top of this API is a lot of work." and the high-level API is equally a pain in the ass. Gah. */ #include #include #include #include #include #include #include #include #include #include /* globals */ GladeXML *xml = NULL; GtkTreeStore *tree = NULL; GtkWidget *treeview = NULL; nw_device_t *thisdev = NULL; /* GRR. When the automatic drag/drop stuff on the treeview moves a row, it fires insert and then delete. Rather than causing excessive flapping on the device, I'll track the action with some globals and just do a rename at the appropriate time */ typedef enum { ModNONE, ModDELETE, ModRENAME, } ModType; ModType RENAMING = ModNONE; GtkTreePath *RENAMEPATH = NULL; /* This is used to make the drag/drop stuff a little clearer */ typedef enum { ObjFOLDER = 0, ObjFILE, ObjEXTERNAL, ObjDEV } ObjType; /* and this clarifies the bits in the tree model */ typedef enum { ColNAME = 0, ColBLOB, ColTYPE, NUMCOLS, } TreeCols; /* this is used for the drop progress stuff */ typedef struct drop_ctx { guint count; guint max; } drop_ctx; /* 'df' */ gboolean capacity( guint *used, guint *total ) { *used = 0; *total = 0; return TRUE; } /* Track copy progress */ void progress( double percent, void *context ) { GtkWidget *sbar = glade_xml_get_widget( xml, "statusbar1" ); guint ctxid; ctxid = gtk_statusbar_get_context_id(GTK_STATUSBAR( sbar ), "status" ); gtk_statusbar_pop( GTK_STATUSBAR( sbar ), ctxid ); if ( percent > 0 ) { gchar *msg = g_strdup_printf( "1/1 %.0f%%", percent * 100 ); gtk_statusbar_push( GTK_STATUSBAR( sbar ), ctxid, msg ); g_free( msg ); } else { guint a, b; if ( capacity( &a, &b )) { } } gtk_main_iteration_do( FALSE ); } /* gtk_tree_model_foreach( GTK_TREE_MODEL( tree ), dumpmodel, NULL );*/ gboolean dumpmodel(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer userdata) { void *data; gtk_tree_model_get( model, iter, ColNAME, &data, -1 ); if ( data != NULL) { /* cool, this works. data is whatever we slung in at gtk_tree_store_set */ fprintf( stderr, "found data %s\n", (char *)data ); } return FALSE; /* Return true to stop walking */ } /* * Sync the model back to the device */ gboolean syncmodel_each( GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer userdata ) { void *data; void *type; gtk_tree_model_get( model, iter, ColBLOB, &data, ColTYPE, &type, -1 ); if ( data ) { int retval; if ( (int *)type == ObjFOLDER ) { nw_folder_t *fptr = (nw_folder_t *)data; retval = nw_add_folder( thisdev, nw_get_folder_name( fptr ), 0 ); if ( retval == 0 ) { /* uh-oh xxx */ } } else { nw_track_t *tptr = (nw_track_t *)data; guint16 folder = g_list_length( nw_get_folderlist( thisdev )); retval = nw_add_to_folder( thisdev, folder, tptr, nw_get_tracknum( tptr ), 0 ); if ( retval == 0 ) { /* uh-oh xxx */ } } } else { /* probably a delete */ } return FALSE; } void syncmodel( GtkTreeModel *model ) { GList *old; fprintf( stderr, "THIS CODE NEEDS REWORK\n" ); old = nw_get_folderlist( thisdev ); /* thisdev->folderlist = NULL; XXX previously, I just nuked the entire folderlist and rebuilt it from scratch. Aside from the fact that that's a bit wasteful, the new opaque nw_device_t doesn't allow direct access to the folderlist in this way. */ nw_set_boolean( thisdev, "autosync", FALSE ); gtk_tree_model_foreach( GTK_TREE_MODEL( tree ), syncmodel_each, NULL ); g_list_free( old ); nw_dev_sync( thisdev ); nw_set_boolean( thisdev, "autosync", TRUE ); } /* * uuuuuugly. edited passes the path to me as a bleedin' STRING. */ void edited( GtkCellRendererText *cellrenderertext, const gchar *cpath, const gchar *new_text ) { GtkTreePath *path = gtk_tree_path_new_from_string( cpath ); GtkTreeIter iter; GValue gval = { 0, }; gtk_tree_model_get_iter( GTK_TREE_MODEL( tree ), &iter, path ); gtk_tree_store_set( tree, &iter, ColNAME, new_text, -1 ); /* update the mple structure */ if ( gtk_tree_path_get_depth( path ) == 1 ) { nw_folder_t *folder; gtk_tree_model_get_value( GTK_TREE_MODEL( tree ), &iter, ColBLOB, &gval ); folder = g_value_get_pointer( &gval ); if ( folder != NULL ) { dprintf( stderr, "renaming folder %s @ %s to %s\n", nw_get_folder_name( folder ), cpath, new_text ); /* only update it if it actually needs updating! */ if ( memcmp( nw_get_folder_name( folder ), new_text, strlen( new_text )) != 0 ) { nw_set_folder_name( folder, new_text ); nw_dev_sync( thisdev ); } } else { fprintf( stderr, "problem getting folder\n" ); } } else if ( gtk_tree_path_get_depth( path ) == 2 ) { nw_track_t *track; gtk_tree_model_get_value( GTK_TREE_MODEL( tree ), &iter, ColBLOB, &gval ); track = g_value_get_pointer( &gval ); if ( track != NULL ) { if ( memcmp( nw_get_tag( track, NW_FILENAME ), new_text, strlen( new_text )) != 0 ) { nw_set_tag( track, NW_FILENAME, new_text ); nw_dev_sync( thisdev ); } } else { fprintf( stderr, "problem getting track\n" ); } } g_value_unset( &gval ); gtk_tree_path_free( path ); } /* * model-frobbing signals */ void row_changed( GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter ) { /* nothing to do! */ } void row_inserted( GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter ) { if ( RENAMING == ModRENAME ) { gchar *pathstring = gtk_tree_path_to_string( path ); /* OUCH */ RENAMEPATH = gtk_tree_path_new_from_string( pathstring ); dprintf( stderr, "renaming %s\n", pathstring ); g_free( pathstring ); } else { /* regular insert; nothing to do */ } } /* * This gets called AFTER the row has been deleted, so you can't pull * the correct values out of the TreeModel... */ void row_deleted( GtkTreeModel *model, GtkTreePath *path ) { ObjType what; GList *fnode, *tnode = NULL; nw_folder_t *fptr; nw_track_t *tptr = NULL; gint *src_idx, *dst_idx = NULL; gchar *src = NULL, *dest = NULL; /* fetch the src & destination index */ src_idx = gtk_tree_path_get_indices( path ); if ( RENAMING == ModRENAME ) { dst_idx = gtk_tree_path_get_indices( RENAMEPATH ); /* if the drop point is before the current point, the src_idx will be out of whack */ if ( gtk_tree_path_get_depth( path ) == 2 ) { if ( dst_idx[1] < src_idx[1] ) { src_idx[1]--; } } if ( dst_idx[0] < src_idx[0] ) { src_idx[0]--; } } /* figure out what we're dropping/renaming */ if ( gtk_tree_path_get_depth( path ) == 1 ) { what = ObjFOLDER; } else if ( gtk_tree_path_get_depth( path ) == 2 ) { what = ObjFILE; } else { fprintf( stderr, "WTF?\n" ); goto out; } /* now locate the thing to be deleted */ fnode = g_list_nth( nw_get_folderlist( thisdev ), src_idx[0] ); if ( fnode == NULL ) { fprintf( stderr, "can't find folder %d (%d folders on device)\n", src_idx[0], g_list_length( nw_get_folderlist( thisdev ))); goto out; } else { fptr = fnode->data; dest = nw_get_folder_name( fptr ); } if ( what == ObjFILE ) { tnode = g_list_nth( nw_get_folder_tracklist( fptr ), src_idx[1] ); if ( tnode == NULL ) { fprintf( stderr, "can't find track %d (%d tracks in folder)\n", src_idx[1], g_list_length( nw_get_folder_tracklist( fptr ))); goto out; } else { tptr = tnode->data; dest = g_strdup_printf( "%s/%s (%d)", dest, nw_get_tag( tptr, NW_FILENAME ), src_idx[1] ); } } if ( RENAMING == ModDELETE ) { if ( what == ObjFILE ) { if ( nw_del_track( thisdev, nw_get_tracknum( tptr )) && nw_dev_sync( thisdev )) { } else { perror( "delete track" ); } } else { if ( nw_del_folder( thisdev, src_idx[0] + 1 ) && nw_dev_sync( thisdev )) { } else { perror( "delete folder" ); } } } else if ( RENAMING == ModRENAME ) { ObjType swhat; GList *sfnode; nw_folder_t *sfptr; if ( gtk_tree_path_get_depth( RENAMEPATH ) == 1 ) { swhat = ObjFOLDER; } else if ( gtk_tree_path_get_depth( RENAMEPATH ) == 2 ) { swhat = ObjFILE; } else { fprintf( stderr, "source wtf?\n" ); } sfnode = g_list_nth( nw_get_folderlist( thisdev ), dst_idx[0] ); if ( sfnode == NULL ) { if ( what == ObjFILE ) { fprintf( stderr, "can't find folder %d (%d folders on device)\n", dst_idx[0], g_list_length( nw_get_folderlist( thisdev ) )); } else { src = g_strdup( "end of device" ); swhat = ObjFOLDER; } } else { sfptr = sfnode->data; src = nw_get_folder_name( sfptr ); } if ( what == ObjFILE ) { fprintf( stderr, "%s -> %s, slot %d\n", dest, src, dst_idx[1] + 1 ); if ( nw_mv_track( thisdev, nw_get_tracknum( tptr ), dst_idx[0] + 1, dst_idx[1] + 1 ) && nw_dev_sync( thisdev )) { goto out; } else { perror( "mple_mv_track" ); } } else { if ( nw_mv_folder( thisdev, src_idx[0] + 1, dst_idx[0] + 1 ) && nw_dev_sync( thisdev )) { goto out; } else { perror( "mple_mv_folder" ); } } } out: /* done with this */ RENAMING= ModNONE; if ( RENAMEPATH != NULL ) { gtk_tree_path_free( RENAMEPATH ); RENAMEPATH = NULL; } } /* * populate the tree on startup */ const GtkTargetEntry row_targets[] = { { "GTK_TREE_MODEL_ROW", GTK_TARGET_SAME_WIDGET, 0 }, { "text/uri-list", 0, 0 }, /* nautilus */ /* mozilla drop types: text/x-moz-url _NETSCAPE_URL text/x-moz-url-data text/x-moz-url-desc text/_moz_htmlcontext text/_moz_htmlinfo text/html text/unicode text/plain */ }; gboolean mp3fm_startup( GtkWidget *widget, GdkEvent *event, gpointer user_data ) { GList *node; GtkTreeIter iter, titer; GtkCellRenderer *renderer; GtkTreeViewColumn *column; GList *players; players = nw_get_devs( NULL ); if ( g_list_length( players ) == 0 ) { fprintf( stderr, "No players detected, bailing\n" ); return FALSE; exit( 1 ); } else if ( g_list_length( players ) > 1 ) { fprintf( stderr, "Multiple players detected, using first one\n" ); } thisdev = g_list_nth_data( players, 0 ); if ( thisdev == NULL ) { perror( "dev init" ); exit( 1 ); } nw_set_progress_func( thisdev, progress ); nw_set_boolean( thisdev, "autosync", FALSE ); /* Woohoo... */ /* clear the progress bar */ progress( 0, NULL ); /* now load up the tree view with the data */ treeview = glade_xml_get_widget( xml, "treeview" ); /* tree consists of a string to display, a link to the folder/track pointer structure for the displayed string, and a flag indicating if it's a track or not */ tree = gtk_tree_store_new( NUMCOLS, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_INT ); if ( nw_parse_directory( thisdev )) { node = nw_get_folderlist( thisdev ); while( node ) { GList *tnode =NULL; nw_track_t *tptr = NULL; gchar *label = NULL; nw_folder_t *fptr = node->data; label = nw_get_folder_name( fptr ); if ( strlen( label ) == 0 ) { g_free( label ); label = g_strdup( "(No Name)" ); } gtk_tree_store_append( tree, &iter, 0 ); gtk_tree_store_set( tree, &iter, ColNAME, label, ColBLOB, fptr, ColTYPE, ObjFOLDER, -1 ); g_free( label ); tnode = nw_get_folder_tracklist( fptr ); while( tnode ) { tptr = tnode->data; label = nw_get_tag( tptr, NW_FILENAME ); gtk_tree_store_append( tree, &titer, &iter ); gtk_tree_store_set( tree, &titer, ColNAME, label, ColBLOB, tptr, ColTYPE, ObjFILE, -1 ); g_free( label ); tnode = g_list_next( tnode ); } node = g_list_next( node ); } } g_signal_connect( G_OBJECT( tree ), "row_changed", G_CALLBACK( row_changed ), NULL ); g_signal_connect( G_OBJECT( tree ), "row_inserted", G_CALLBACK( row_inserted ), NULL ); g_signal_connect( G_OBJECT( tree ), "row_deleted", G_CALLBACK( row_deleted ), NULL ); gtk_tree_view_set_model( GTK_TREE_VIEW(treeview), GTK_TREE_MODEL(tree)); renderer = gtk_cell_renderer_text_new (); g_object_set( G_OBJECT( renderer ), "editable", TRUE, NULL ); g_signal_connect( G_OBJECT( renderer ), "edited", G_CALLBACK( edited ), NULL ); column = gtk_tree_view_column_new_with_attributes ( "TITLE", renderer, "text", 0, NULL); gtk_tree_view_append_column( GTK_TREE_VIEW (treeview), column ); /* further startup frobbery - this allows me to override what the treeview thinks it can accept */ gtk_tree_view_enable_model_drag_dest( GTK_TREE_VIEW( treeview ), row_targets, G_N_ELEMENTS( row_targets ), GDK_ACTION_MOVE ); return TRUE; } /* * clean up */ void shutdown( GtkButton *widget, gpointer user_data ) { if ( thisdev != NULL ) { nw_dev_free( thisdev ); } gtk_exit( 0 ); } /* * noop for the moment */ void on_mount_device1_activate( GtkMenuItem *widget, gpointer user_data ) { GtkWidget *contextmenu = glade_xml_get_widget( xml, "contextmenu" ); gtk_widget_hide( contextmenu ); /* system( "/bin/mount /media/NW-S23" ); */ } /* * pop up the context menu */ gboolean button_press( GtkWidget *widget, GdkEventButton *event, gpointer user_data ) { if ( event->button == 3 ) { /* doesn't appear to be working, for some reason */ GtkWidget *contextmenu = glade_xml_get_widget( xml, "contextmenu" ); fprintf( stderr, "showing context menu %p\n", contextmenu ); gtk_widget_show_all( contextmenu ); return TRUE; } else { } return FALSE; /* propagate */ } /* * Handle delete button */ void on_delButton_clicked( GtkButton *widget, gpointer user_data ) { GtkTreeSelection *selected; GtkTreeModel *model = GTK_TREE_MODEL( tree ); GList *paths; GtkTreeIter iter; void *data; /* get the currently selected tree item */ selected = gtk_tree_view_get_selection( GTK_TREE_VIEW( treeview )); if ( selected == NULL ) { return; } paths = gtk_tree_selection_get_selected_rows( selected, &model ); RENAMING = ModDELETE; while( paths ) { GtkTreePath *path = paths->data; gtk_tree_model_get_iter( model, &iter, path ); gtk_tree_model_get( model, &iter, ColNAME, &data, -1 ); gtk_tree_store_remove( tree, &iter ); paths = g_list_remove( paths, path ); gtk_tree_path_free( path ); } return; } void on_delAllButton_clicked( GtkButton *widget, gpointer user_data ) { GtkWidget *confirm; GtkWidget *message; confirm = glade_xml_get_widget( xml, "delete_ok_dialogue" ); message = glade_xml_get_widget( xml, "delete_ok_label" ); gtk_label_set_text( GTK_LABEL( message ), "Delete all files?" ); gtk_widget_show( confirm ); } /* * attempt to verify that a legal drop is being * performed. Specifically: * - don't allow a folder to be dropped inside a folder * - don't allow a file to be dropped on another file * - don't allow a file to be dropped outside a folder * Since I can't tell much about an externally dropped file at this * point in the process, that case is less rigourously checked */ gboolean drag_drop_ok(GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time) { GtkTreePath *path; GList *paths; GtkTreeModel *model = GTK_TREE_MODEL( tree ); GtkTreeSelection *selected; ObjType source, dest; GValue gval = { 0, }; GtkTreeViewDropPosition pos; gboolean destexists; if ( gtk_drag_get_source_widget( context ) != NULL ) { selected = gtk_tree_view_get_selection( GTK_TREE_VIEW( treeview )); if ( selected != NULL ) { GtkTreePath *path; GtkTreeIter iter; if (( paths = gtk_tree_selection_get_selected_rows( selected, &model )) != NULL ) { /* generic solution, we'd have to cater for multiple selections. the treeview is single-select only... */ path = paths->data; gtk_tree_model_get_iter( model, &iter, path ); gtk_tree_model_get_value( model, &iter, ColTYPE, &gval ); source = g_value_get_int( &gval ); g_value_unset( &gval ); gtk_tree_path_free( path ); } else { /* nothing selected... */ return TRUE; } } else { return TRUE; } } else { /* drop from external source e.g. nautilus * can't tell what we'll do with it until it's actually dropped, though. */ GList *node = context->targets; source = ObjDEV; while( node ) { char *possible_type; int i; possible_type = gdk_atom_name ((GdkAtom) node->data); /* do this in such a way that I don't have to redo it if I expand the row_targets list */ for ( i = 0; i < sizeof( row_targets )/sizeof( GtkTargetEntry ); i++ ) { if ( strcmp( possible_type, row_targets[i].target ) == 0 ) { source = ObjEXTERNAL; } #ifdef DEBUG else { fprintf( stderr, "can't use type [%s]\n", possible_type ); } #endif } g_free( possible_type ); if ( source != ObjDEV ) { break; } node = g_list_next( node ); } if ( source == ObjDEV ) { #ifdef DEBUG fprintf( stderr, "bailing out, no useful types found\n" ); #endif return TRUE; } } /* identify what we're dropping on */ destexists = gtk_tree_view_get_dest_row_at_pos( GTK_TREE_VIEW( widget ), x, y, &path, &pos ); if ( path != NULL ) { GtkTreeIter iter; GValue val = { 0, }; gtk_tree_model_get_iter( model, &iter, path ); gtk_tree_model_get_value( model, &iter, ColTYPE, &val ); dest = g_value_get_int( &val ); g_value_unset( &val ); #ifdef DEBUG { gchar *p; gtk_tree_model_get_value( model, &iter, ColNAME, &val ); p = gtk_tree_path_to_string( path ); fprintf( stderr, "drop path: %s (%s, %s)\n", p, g_value_get_string( &val ), dest == ObjFILE ? "file" : "folder" ); g_value_unset( &val ); g_free( p ); } #endif } else { dest = ObjDEV; /* dropped outside existing stuff */ } if ( source == ObjFOLDER ) { if ( destexists && ( pos == GTK_TREE_VIEW_DROP_INTO_OR_BEFORE || pos == GTK_TREE_VIEW_DROP_INTO_OR_AFTER )) { #ifdef DEBUG fprintf( stderr, "can't drop folder into existing folder/file\n" ); #endif return TRUE; } } else { /* source is a file or an external */ switch( dest ) { default: case ObjDEV: if ( source == ObjEXTERNAL ) { return FALSE; } #ifdef DEBUG fprintf( stderr, "Can't drop file on device\n" ); #endif return TRUE; break; case ObjFILE: if ( destexists && ( pos != GTK_TREE_VIEW_DROP_BEFORE && pos != GTK_TREE_VIEW_DROP_AFTER )) { #ifdef DEBUG /* ideally we'd treat this as a drop before, assuming there's a way for me to pass that back to the caller */ fprintf( stderr, "can't drop anything on existing file (%d)\n", pos ); #endif return TRUE; } break; case ObjFOLDER: if ( source == ObjEXTERNAL ) { return FALSE; } if ( !destexists ) { #ifdef DEBUG fprintf( stderr, "can't drop file on non-existent folder\n" ); #endif return TRUE; } if ( pos != GTK_TREE_VIEW_DROP_INTO_OR_BEFORE && pos != GTK_TREE_VIEW_DROP_INTO_OR_AFTER ) { #ifdef DEBUG fprintf( stderr, "can't drop file outside folder\n" ); #endif return TRUE; } break; } } return FALSE; } gboolean drag_drop( GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, gpointer user_data ) { if ( drag_drop_ok( widget, context, x, y, time ) == TRUE ) { return TRUE; /* not accepting this, thanks */ } return FALSE; } /* * find all files in a tree. Dumb, susceptible to loops, etc. * I'm planning on adding a hook function which will only collect * compatible files (MP3, 44k1 sampling rate, 8-320kbit) and give up * when it's reached the capacity of the device. For now, just be * careful, eh? */ GList *findfiles( const gchar *start, GList *files ) { GDir *dir; GError *error; if ( g_file_test( start, G_FILE_TEST_IS_DIR )) { const gchar *readname; dir = g_dir_open( start, 0, &error ); if ( dir == NULL ) { return files; } while(( readname = g_dir_read_name( dir )) != NULL ) { gchar *newname; /* these appear to be helpfully removed for me, but just in case... */ if (( strcmp( readname, "." ) == 0 ) || ( strcmp( readname, ".." ) == 0 )) { continue; } /* this leaks memory pretty badly, but apparently by design. */ newname = g_build_path( "/", start, readname, NULL ); /* testing here saves some recursion */ if ( g_file_test( newname, G_FILE_TEST_IS_DIR )) { files = findfiles( newname, files ); g_free( newname ); } else { files = g_list_append( files, newname ); /* gets freed at cleanup */ } } g_dir_close( dir ); } else { files = g_list_append( files, g_strdup( start )); } return files; } /* * This is called when a drop is made. * * At present it's a mess. Realistically, it should first determine * candidate files, then it should do the copy. */ void drag_data_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection_data, guint info, guint time_, gpointer user_data) { GtkTreePath *path; GtkTreeModel *model = GTK_TREE_MODEL( tree ); GtkTreeIter iter, *folder_iter = NULL, track_iter, parent; ObjType source, dest = ObjFILE; /* ehhh. */ gboolean destexists; GtkTreeViewDropPosition pos; GValue gval = { 0, }; gchar *srcuri, *destfolder; guint16 folder, idx, newtrack; nw_folder_t *fptr; gchar **files; static gchar *newfolder = "New Album"; /* integrating new findfiles() function... */ GList *filelist = NULL; GList *node = NULL; if ( selection_data == NULL ) { return; } /* don't interfere with the internal stuff */ if ( gtk_drag_get_source_widget( context ) != NULL ) { RENAMING = ModRENAME; return; } srcuri = g_strndup( (const gchar *)selection_data->data, selection_data->length ); files = g_strsplit( srcuri, "\r\n", 0 ); /* It is possible that we're dealing with multiple dropped items, so deal with them all */ for ( idx = 0; files[idx] != NULL; idx++ ) { gchar *thisfile, *thisdir; /* find out what the source is */ if ( g_str_has_prefix( files[idx], "file://" )) { struct stat statbuf; gchar *hostname; GError *e; hostname = NULL; e = NULL; thisfile = g_filename_from_uri( files[idx], &hostname, &e ); /* discard trailing CRLF */ if ( g_str_has_suffix( thisfile, "\r\n" )) { thisfile[strlen( thisfile ) - 2 ] = '\0'; } if ( stat( thisfile, &statbuf ) != 0 ) { fprintf( stderr, "error statting %s: %s\n", thisfile, strerror( errno )); goto loop_end; } if ( S_ISDIR( statbuf.st_mode )) { source = ObjFOLDER; destfolder = (gchar *)g_basename( thisfile ); thisdir = g_strdup( thisfile ); } else { source = ObjFILE; destfolder = NULL; } } else { /* we don't handle other URIs just yet! */ goto loop_end; } /* identify the target of the drop */ destexists = gtk_tree_view_get_dest_row_at_pos( GTK_TREE_VIEW( widget ), x, y, &path, &pos ); if ( path != NULL ) { gtk_tree_model_get_iter( model, &iter, path ); gtk_tree_model_get_value( model, &iter, ColTYPE, &gval ); dest = g_value_get_int( &gval ); g_value_unset( &gval ); if ( destfolder == NULL ) { if ( dest == ObjFOLDER ) { gtk_tree_model_get_value( model, &iter, ColNAME, &gval ); destfolder = (gchar *)g_value_get_string( &gval ); folder_iter = &iter; } else { gtk_tree_model_iter_parent( model, &parent, &iter ); gtk_tree_model_get_value( model, &parent, ColNAME, &gval ); destfolder = (gchar *)g_value_get_string( &gval ); folder_iter = &parent; } } } else { if ( source == ObjFILE ) { dest = ObjDEV; /* dropped outside existing stuff */ destfolder = newfolder; } } /* determine if we allow the drop to happen */ if ( source == ObjFOLDER ) { if ( destexists && ( pos == GTK_TREE_VIEW_DROP_INTO_OR_BEFORE || pos == GTK_TREE_VIEW_DROP_INTO_OR_AFTER )) { goto loop_end; } } else { /* source is a file or an external */ switch( dest ) { case ObjDEV: dest = ObjFOLDER; break; case ObjFILE: /* files only get dropped before and after - no into */ if ( pos != GTK_TREE_VIEW_DROP_BEFORE && pos != GTK_TREE_VIEW_DROP_AFTER ) { goto loop_end; } break; default: case ObjFOLDER: if ( pos != GTK_TREE_VIEW_DROP_INTO_OR_BEFORE && pos != GTK_TREE_VIEW_DROP_INTO_OR_AFTER ) { goto loop_end; } break; } } /* look up the destination folder */ folder = nw_get_folder( thisdev, destfolder, 0 ); if ( !folder ) { folder = nw_add_folder( thisdev, destfolder, 0 ); if ( !folder ) { goto loop_end; } /* added new folder */ fptr = g_list_nth_data( nw_get_folderlist( thisdev ), folder - 1 ); gtk_tree_store_append( tree, &parent, 0 ); gtk_tree_store_set( tree, &parent, ColNAME, g_strdup( destfolder ), ColBLOB, fptr, ColTYPE, ObjFOLDER, -1 ); folder_iter = &parent; } if ( source == ObjFOLDER ) { filelist = findfiles( thisfile, filelist ); node = filelist; } while( 1 ) { guint16 ind = 0; if ( source == ObjFOLDER ) { thisfile = node->data; if (( node = g_list_next( node )) == NULL ) { break; } } /* where in the device are we putting the file? */ if ( source == ObjFILE ) { gchar *p; if ( path != NULL ) { p = gtk_tree_path_to_string( path ); /* path == 1 -> dropped on a folder == 2 -> dropped on a file position, which we care about */ if ( gtk_tree_path_get_depth( path ) == 2 ) { gint *indices = gtk_tree_path_get_indices( path ); ind = indices[gtk_tree_path_get_depth( path ) - 1] + 1; } g_free( p ); } } newtrack = nw_add_track( thisdev, thisfile, NULL, folder, ind ); if ( nw_dev_sync( thisdev ) == 0 ) { perror( "sync" ); } if ( newtrack ) { nw_track_t *tptr; gchar *label; /* for some reason this version of fptr doesn't get updated when the above call modifies the folder's tracklist. Not sure why that is, but this refreshes it correctly. */ fptr = g_list_nth_data( nw_get_folderlist( thisdev ), folder - 1 ); tptr = g_list_nth_data( nw_get_folder_tracklist( fptr ), newtrack - 1 ); label = nw_get_tag( tptr, NW_FILENAME ); /* make sure the drop position is correct */ if ( source == ObjFOLDER && pos == GTK_TREE_VIEW_DROP_BEFORE ) { ind = ind - 1; } gtk_tree_store_insert( tree, &track_iter, folder_iter, ind ); gtk_tree_store_set( tree, &track_iter, ColNAME, label, ColBLOB, tptr, ColTYPE, ObjFILE, -1 ); } else { fprintf( stderr, "failed to add track %s: %s\n", thisfile, strerror( errno )); } /* only adding a single file? time to get out! */ if ( source == ObjFILE ) { break; } } loop_end: if ( G_IS_VALUE( &gval )) { g_value_unset( &gval ); } /* clean up the filelist if we used it */ if ( filelist != NULL ) { node = filelist; while( node ) { g_free( node->data ); node = g_list_next( node ); } g_list_free( filelist ); filelist = NULL; } } g_strfreev( files ); g_free( srcuri ); /* clear the progress bar */ progress( 0, NULL ); } /* return a cue as to whether the current drag/drop is possible */ /* as soon as this returns true, the entire drag breaks. GRR. */ gboolean drag_motion( GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, gpointer user_data ) { if ( drag_drop_ok( widget, context, x, y, time ) == TRUE ) { /* not possible */ context->action = 0; } else { /* possible */ if ( gtk_drag_get_source_widget( context ) != NULL ) { context->action = GDK_ACTION_MOVE; } else { context->action = GDK_ACTION_COPY; } } return FALSE; } /* * double-click on a row to open/close it. This is actually superceded * by the fact that doubleclicking on a row will /edit/ it... */ gboolean on_treeview_row_activated( GtkWidget *widget, GtkTreePath *path, GtkTreeViewColumn *column ) { if (!gtk_tree_view_row_expanded( GTK_TREE_VIEW(widget), path )) { gtk_tree_view_expand_row( GTK_TREE_VIEW(widget), path, FALSE ); } else { gtk_tree_view_collapse_row( GTK_TREE_VIEW(widget), path ); } return TRUE; } /* * the fun starts here */ int main( int argc, char *argv[] ) { gtk_init( &argc, &argv ); /* load the interface */ xml = glade_xml_new( "mp3filemanager.glade", NULL, NULL ); /* connect the signals in the interface */ glade_xml_signal_autoconnect( xml ); /* start the event loop */ gtk_main(); return 0; }