Mercurial > geeqie
view src/dupe.c @ 622:54e20abb5c6d
Fix display of collection in overlay info.
Due to markup escaped <i>collection</i> was displayed instead of collection's in italic.
Overlay info syntax was extended to allow the wrapping of displayed data with markup.
General syntax is: %name[:length limit][:extra]%
Extra string uses special character '*' to mark the place of the data to display.
If no '*' is present, then extra string is just appended to data.
Any "\n" is replaced by a newline on display.
Pango mark up is accepted in left and right parts.
If data is empty, nothing will be displayed.
Examples:
"%name:<i>*</i>\n%" -> name is displayed in italics ended with a newline
"%size:\n%" -> size is displayed with a newline at end
"%formatted.ISOSpeedRating:ISO *%" -> prefix iso number with "ISO " (ie. "ISO 100")
"Collection <b>*</b>\n" -> display collection name in bold prefixed by "Collection " and a newline is appended
Collection name formatting was slighly improved by not displaying the .gqv extension.
The default overlay info string was modified to use the new syntax, but older info strings should be
displayed as usual.
author | zas_ |
---|---|
date | Sat, 10 May 2008 21:29:53 +0000 |
parents | 905688aa2317 |
children | e34c1002e553 |
line wrap: on
line source
/* * Geeqie * (C) 2005 John Ellis * Copyright (C) 2008 The Geeqie Team * * Author: John Ellis * * This software is released under the GNU General Public License (GNU GPL). * Please read the included file COPYING for more information. * This software comes with no warranty of any kind, use at your own risk! */ #include "main.h" #include "dupe.h" #include "cache.h" #include "collect.h" #include "collect-table.h" #include "debug.h" #include "dnd.h" #include "editors.h" #include "filedata.h" #include "image-load.h" #include "img-view.h" #include "info.h" #include "layout.h" #include "layout_image.h" #include "md5-util.h" #include "menu.h" #include "print.h" #include "thumb.h" #include "utilops.h" #include "ui_bookmark.h" #include "ui_fileops.h" #include "ui_menu.h" #include "ui_misc.h" #include "ui_tree_edit.h" #include <gdk/gdkkeysyms.h> /* for keyboard values */ #include <math.h> #define DUPE_DEF_WIDTH 600 #define DUPE_DEF_HEIGHT 400 /* column assignment order (simply change them here) */ enum { DUPE_COLUMN_POINTER = 0, DUPE_COLUMN_RANK, DUPE_COLUMN_THUMB, DUPE_COLUMN_NAME, DUPE_COLUMN_SIZE, DUPE_COLUMN_DATE, DUPE_COLUMN_DIMENSIONS, DUPE_COLUMN_PATH, DUPE_COLUMN_COLOR, DUPE_COLUMN_COUNT /* total columns */ }; static GList *dupe_window_list = NULL; /* list of open DupeWindow *s */ /* * Well, after adding the 'compare two sets' option things got a little sloppy in here * because we have to account for two 'modes' everywhere. (be careful). */ static void dupe_match_unlink(DupeItem *a, DupeItem *b); static DupeItem *dupe_match_find_parent(DupeWindow *dw, DupeItem *child); static gint dupe_match(DupeItem *a, DupeItem *b, DupeMatchType mask, gdouble *rank, gint fast); static void dupe_thumb_step(DupeWindow *dw); static gint dupe_check_cb(gpointer data); static void dupe_second_add(DupeWindow *dw, DupeItem *di); static void dupe_second_remove(DupeWindow *dw, DupeItem *di); static GtkWidget *dupe_menu_popup_second(DupeWindow *dw, DupeItem *di); static void dupe_dnd_init(DupeWindow *dw); /* * ------------------------------------------------------------------ * Window updates * ------------------------------------------------------------------ */ static void dupe_window_update_count(DupeWindow *dw, gint count_only) { gchar *text; if (!dw->list) { text = g_strdup(_("Drop files to compare them.")); } else if (count_only) { text = g_strdup_printf(_("%d files"), g_list_length(dw->list)); } else { text = g_strdup_printf(_("%d matches found in %d files"), g_list_length(dw->dupes), g_list_length(dw->list)); } if (dw->second_set) { gchar *buf = g_strconcat(text, " ", _("[set 1]"), NULL); g_free(text); text = buf; } gtk_label_set_text(GTK_LABEL(dw->status_label), text); g_free(text); } static guint64 msec_time(void) { struct timeval tv; if (gettimeofday(&tv, NULL) == -1) return 0; return (guint64)tv.tv_sec * 1000000 + (guint64)tv.tv_usec; } static gint dupe_iterations(gint n) { return (n * ((n + 1) / 2)); } static void dupe_window_update_progress(DupeWindow *dw, const gchar *status, gdouble value, gint force) { const gchar *status_text; if (status) { guint64 new_time = 0; if (dw->setup_n % 10 == 0) { new_time = msec_time() - dw->setup_time; } if (!force && value != 0.0 && dw->setup_count > 0 && new_time > 2000000) { gchar *buf; gint t; gint d; guint32 rem; if (new_time - dw->setup_time_count < 250000) return; dw->setup_time_count = new_time; if (dw->setup_done) { if (dw->second_set) { t = dw->setup_count; d = dw->setup_count - dw->setup_n; } else { t = dupe_iterations(dw->setup_count); d = dupe_iterations(dw->setup_count - dw->setup_n); } } else { t = dw->setup_count; d = dw->setup_count - dw->setup_n; } rem = (t - d) ? ((gdouble)(dw->setup_time_count / 1000000) / (t - d)) * d : 0; gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), value); buf = g_strdup_printf("%s %d:%02d ", status, rem / 60, rem % 60); gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), buf); g_free(buf); return; } else if (force || value == 0.0 || dw->setup_count == 0 || dw->setup_time_count == 0 || (new_time > 0 && new_time - dw->setup_time_count >= 250000)) { if (dw->setup_time_count == 0) dw->setup_time_count = 1; if (new_time > 0) dw->setup_time_count = new_time; gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), value); status_text = status; } else { status_text = NULL; } } else { gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), 0.0); status_text = " "; } if (status_text) gtk_progress_bar_set_text(GTK_PROGRESS_BAR(dw->extra_label), status_text); } static void widget_set_cursor(GtkWidget *widget, gint icon) { GdkCursor *cursor; if (!widget->window) return; if (icon == -1) { cursor = NULL; } else { cursor = gdk_cursor_new(icon); } gdk_window_set_cursor(widget->window, cursor); if (cursor) gdk_cursor_unref(cursor); } /* * ------------------------------------------------------------------ * row color utils * ------------------------------------------------------------------ */ static void dupe_listview_realign_colors(DupeWindow *dw) { GtkTreeModel *store; GtkTreeIter iter; gboolean color_set = TRUE; DupeItem *parent = NULL; gint valid; store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)); valid = gtk_tree_model_get_iter_first(store, &iter); while (valid) { DupeItem *child; DupeItem *child_parent; gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &child, -1); child_parent = dupe_match_find_parent(dw, child); if (!parent || parent != child_parent) { if (!parent) { /* keep the first row as it is */ gtk_tree_model_get(store, &iter, DUPE_COLUMN_COLOR, &color_set, -1); } else { color_set = !color_set; } parent = dupe_match_find_parent(dw, child); } gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_COLOR, color_set, -1); valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter); } } /* * ------------------------------------------------------------------ * Dupe item utils * ------------------------------------------------------------------ */ static DupeItem *dupe_item_new(FileData *fd) { DupeItem *di; di = g_new0(DupeItem, 1); di->fd = file_data_ref(fd); di->group = NULL; di->group_rank = 0.0; di->simd = NULL; di->checksum = 0; di->md5sum = NULL; di->width = 0; di->height = 0; di->second = FALSE; return di; } static void dupe_item_free(DupeItem *di) { file_data_unref(di->fd); image_sim_free(di->simd); g_free(di->md5sum); if (di->pixbuf) g_object_unref(di->pixbuf); g_free(di); } static void dupe_list_free(GList *list) { GList *work = list; while (work) { DupeItem *di = work->data; work = work->next; dupe_item_free(di); } g_list_free(list); } /* static DupeItem *dupe_item_find_fd_by_list(FileData *fd, GList *work) { while (work) { DupeItem *di = work->data; if (di->fd == fd) return di; work = work->next; } return NULL; } */ /* static DupeItem *dupe_item_find_fd(DupeWindow *dw, FileData *fd) { DupeItem *di; di = dupe_item_find_fd_by_list(fd, dw->list); if (!di && dw->second_set) di = dupe_item_find_fd_by_list(fd, dw->second_list); return di; } */ static DupeItem *dupe_item_find_path_by_list(const gchar *path, GList *work) { while (work) { DupeItem *di = work->data; if (strcmp(di->fd->path, path) == 0) return di; work = work->next; } return NULL; } static DupeItem *dupe_item_find_path(DupeWindow *dw, const gchar *path) { DupeItem *di; di = dupe_item_find_path_by_list(path, dw->list); if (!di && dw->second_set) di = dupe_item_find_path_by_list(path, dw->second_list); return di; } /* * ------------------------------------------------------------------ * Image property cache * ------------------------------------------------------------------ */ static void dupe_item_read_cache(DupeItem *di) { gchar *path; CacheData *cd; if (!di) return; path = cache_find_location(CACHE_TYPE_SIM, di->fd->path); if (!path) return; if (filetime(di->fd->path) != filetime(path)) { g_free(path); return; } cd = cache_sim_data_load(path); g_free(path); if (cd) { if (!di->simd && cd->sim) { di->simd = cd->sim; cd->sim = NULL; } if (di->width == 0 && di->height == 0 && cd->dimensions) { di->width = cd->width; di->height = cd->height; } if (di->checksum == 0 && cd->have_checksum) { di->checksum = cd->checksum; } if (!di->md5sum && cd->have_md5sum) { di->md5sum = md5_digest_to_text(cd->md5sum); } cache_sim_data_free(cd); } } static void dupe_item_write_cache(DupeItem *di) { gchar *base; mode_t mode = 0755; if (!di) return; base = cache_get_location(CACHE_TYPE_SIM, di->fd->path, FALSE, &mode); if (cache_ensure_dir_exists(base, mode)) { CacheData *cd; cd = cache_sim_data_new(); cd->path = cache_get_location(CACHE_TYPE_SIM, di->fd->path, TRUE, NULL); if (di->width != 0) cache_sim_data_set_dimensions(cd, di->width, di->height); if (di->checksum != 0) cache_sim_data_set_checksum(cd, di->checksum); if (di->md5sum) { guchar digest[16]; if (md5_digest_from_text(di->md5sum, digest)) cache_sim_data_set_md5sum(cd, digest); } if (di->simd) cache_sim_data_set_similarity(cd, di->simd); if (cache_sim_data_save(cd)) { filetime_set(cd->path, filetime(di->fd->path)); } cache_sim_data_free(cd); } g_free(base); } /* * ------------------------------------------------------------------ * Window list utils * ------------------------------------------------------------------ */ static gint dupe_listview_find_item(GtkListStore *store, DupeItem *item, GtkTreeIter *iter) { gint valid; gint row = 0; valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), iter); while (valid) { DupeItem *item_n; gtk_tree_model_get(GTK_TREE_MODEL(store), iter, DUPE_COLUMN_POINTER, &item_n, -1); if (item_n == item) return row; valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(store), iter); row++; } return -1; } static void dupe_listview_add(DupeWindow *dw, DupeItem *parent, DupeItem *child) { DupeItem *di; gint row; gchar *text[DUPE_COLUMN_COUNT]; GtkListStore *store; GtkTreeIter iter; gboolean color_set = FALSE; gint rank; if (!parent) return; store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview))); if (child) { DupeMatch *dm; row = dupe_listview_find_item(store, parent, &iter); gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_COLOR, &color_set, -1); row++; if (child->group) { dm = child->group->data; rank = (gint)floor(dm->rank); } else { rank = 1; printf("NULL group in item!\n"); } } else { if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter)) { gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, DUPE_COLUMN_COLOR, &color_set, -1); color_set = !color_set; } else { color_set = FALSE; } row = 0; rank = 0; } di = (child) ? child : parent; if (!child && dw->second_set) { text[DUPE_COLUMN_RANK] = g_strdup("[1]"); } else if (rank == 0) { text[DUPE_COLUMN_RANK] = g_strdup((di->second) ? "(2)" : ""); } else { text[DUPE_COLUMN_RANK] = g_strdup_printf("%d%s", rank, (di->second) ? " (2)" : ""); } text[DUPE_COLUMN_THUMB] = ""; text[DUPE_COLUMN_NAME] = (gchar *)di->fd->name; text[DUPE_COLUMN_SIZE] = text_from_size(di->fd->size); text[DUPE_COLUMN_DATE] = (gchar *)text_from_time(di->fd->date); if (di->width > 0 && di->height > 0) { text[DUPE_COLUMN_DIMENSIONS] = g_strdup_printf("%d x %d", di->width, di->height); } else { text[DUPE_COLUMN_DIMENSIONS] = g_strdup(""); } text[DUPE_COLUMN_PATH] = di->fd->path; text[DUPE_COLUMN_COLOR] = NULL; gtk_list_store_insert(store, &iter, row); gtk_list_store_set(store, &iter, DUPE_COLUMN_POINTER, di, DUPE_COLUMN_RANK, text[DUPE_COLUMN_RANK], DUPE_COLUMN_THUMB, NULL, DUPE_COLUMN_NAME, text[DUPE_COLUMN_NAME], DUPE_COLUMN_SIZE, text[DUPE_COLUMN_SIZE], DUPE_COLUMN_DATE, text[DUPE_COLUMN_DATE], DUPE_COLUMN_DIMENSIONS, text[DUPE_COLUMN_DIMENSIONS], DUPE_COLUMN_PATH, text[DUPE_COLUMN_PATH], DUPE_COLUMN_COLOR, color_set, -1); g_free(text[DUPE_COLUMN_RANK]); g_free(text[DUPE_COLUMN_SIZE]); g_free(text[DUPE_COLUMN_DIMENSIONS]); } static void dupe_listview_populate(DupeWindow *dw) { GtkListStore *store; GList *work; store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview))); gtk_list_store_clear(store); work = g_list_last(dw->dupes); while (work) { DupeItem *parent = work->data; GList *temp; dupe_listview_add(dw, parent, NULL); temp = g_list_last(parent->group); while (temp) { DupeMatch *dm = temp->data; DupeItem *child; child = dm->di; dupe_listview_add(dw, parent, child); temp = temp->prev; } work = work->prev; } gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->listview)); } static void dupe_listview_remove(DupeWindow *dw, DupeItem *di) { GtkListStore *store; GtkTreeIter iter; gint row; if (!di) return; store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview))); row = dupe_listview_find_item(store, di, &iter); if (row < 0) return; tree_view_move_cursor_away(GTK_TREE_VIEW(dw->listview), &iter, TRUE); gtk_list_store_remove(store, &iter); if (g_list_find(dw->dupes, di) != NULL) { if (!dw->color_frozen) dupe_listview_realign_colors(dw); } } static GList *dupe_listview_get_filelist(DupeWindow *dw, GtkWidget *listview) { GtkTreeModel *store; GtkTreeIter iter; gint valid; GList *list = NULL; store = gtk_tree_view_get_model(GTK_TREE_VIEW(listview)); valid = gtk_tree_model_get_iter_first(store, &iter); while (valid) { DupeItem *di; gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1); list = g_list_prepend(list, file_data_ref(di->fd)); valid = gtk_tree_model_iter_next(store, &iter); } return g_list_reverse(list); } static GList *dupe_listview_get_selection(DupeWindow *dw, GtkWidget *listview) { GtkTreeModel *store; GtkTreeSelection *selection; GList *slist; GList *list = NULL; GList *work; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview)); slist = gtk_tree_selection_get_selected_rows(selection, &store); work = slist; while (work) { GtkTreePath *tpath = work->data; DupeItem *di = NULL; GtkTreeIter iter; gtk_tree_model_get_iter(store, &iter, tpath); gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1); if (di) { list = g_list_prepend(list, file_data_ref(di->fd)); } work = work->next; } g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL); g_list_free(slist); return g_list_reverse(list); } static gint dupe_listview_item_is_selected(DupeWindow *dw, DupeItem *di, GtkWidget *listview) { GtkTreeModel *store; GtkTreeSelection *selection; GList *slist; GList *work; gint found = FALSE; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview)); slist = gtk_tree_selection_get_selected_rows(selection, &store); work = slist; while (!found && work) { GtkTreePath *tpath = work->data; DupeItem *di_n; GtkTreeIter iter; gtk_tree_model_get_iter(store, &iter, tpath); gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di_n, -1); if (di_n == di) found = TRUE; work = work->next; } g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL); g_list_free(slist); return found; } static void dupe_listview_select_dupes(DupeWindow *dw, gint parents) { GtkTreeModel *store; GtkTreeSelection *selection; GtkTreeIter iter; gint valid; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview)); gtk_tree_selection_unselect_all(selection); store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)); valid = gtk_tree_model_get_iter_first(store, &iter); while (valid) { DupeItem *di; gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1); if ( (dupe_match_find_parent(dw, di) == di) == (parents) ) { gtk_tree_selection_select_iter(selection, &iter); } valid = gtk_tree_model_iter_next(store, &iter); } } /* * ------------------------------------------------------------------ * Match group manipulation * ------------------------------------------------------------------ */ static DupeMatch *dupe_match_find_match(DupeItem *child, DupeItem *parent) { GList *work; work = parent->group; while (work) { DupeMatch *dm = work->data; if (dm->di == child) return dm; work = work->next; } return NULL; } static void dupe_match_link_child(DupeItem *child, DupeItem *parent, gdouble rank) { DupeMatch *dm; dm = g_new0(DupeMatch, 1); dm->di = child; dm->rank = rank; parent->group = g_list_append(parent->group, dm); } static void dupe_match_link(DupeItem *a, DupeItem *b, gdouble rank) { dupe_match_link_child(a, b, rank); dupe_match_link_child(b, a, rank); } static void dupe_match_unlink_child(DupeItem *child, DupeItem *parent) { DupeMatch *dm; dm = dupe_match_find_match(child, parent); if (dm) { parent->group = g_list_remove(parent->group, dm); g_free(dm); } } static void dupe_match_unlink(DupeItem *a, DupeItem *b) { dupe_match_unlink_child(a, b); dupe_match_unlink_child(b, a); } static void dupe_match_link_clear(DupeItem *parent, gint unlink_children) { GList *work; work = parent->group; while (work) { DupeMatch *dm = work->data; work = work->next; if (unlink_children) dupe_match_unlink_child(parent, dm->di); g_free(dm); } g_list_free(parent->group); parent->group = NULL; parent->group_rank = 0.0; } static gint dupe_match_link_exists(DupeItem *child, DupeItem *parent) { return (dupe_match_find_match(child, parent) != NULL); } static gdouble dupe_match_link_rank(DupeItem *child, DupeItem *parent) { DupeMatch *dm; dm = dupe_match_find_match(child, parent); if (dm) return dm->rank; return 0.0; } static DupeItem *dupe_match_highest_rank(DupeItem *child) { DupeMatch *dr; GList *work; dr = NULL; work = child->group; while (work) { DupeMatch *dm = work->data; if (!dr || dm->rank > dr->rank) dr = dm; work = work->next; } return (dr) ? dr->di : NULL; } static void dupe_match_rank_update(DupeItem *parent) { GList *work; gdouble rank = 0.0; gint c = 0; work = parent->group; while (work) { DupeMatch *dm = work->data; work = work->next; rank += dm->rank; c++; } if (c > 0) { parent->group_rank = rank / c; } else { parent->group_rank = 0.0; } } static DupeItem *dupe_match_find_parent(DupeWindow *dw, DupeItem *child) { GList *work; if (g_list_find(dw->dupes, child)) return child; work = child->group; while (work) { DupeMatch *dm = work->data; if (g_list_find(dw->dupes, dm->di)) return dm->di; work = work->next; } return NULL; } static void dupe_match_reset_list(GList *work) { while (work) { DupeItem *di = work->data; work = work->next; dupe_match_link_clear(di, FALSE); } } static void dupe_match_reparent(DupeWindow *dw, DupeItem *old, DupeItem *new) { GList *work; if (!old || !new || !dupe_match_link_exists(old, new)) return; dupe_match_link_clear(new, TRUE); work = old->group; while (work) { DupeMatch *dm = work->data; dupe_match_unlink_child(old, dm->di); dupe_match_link_child(new, dm->di, dm->rank); work = work->next; } new->group = old->group; old->group = NULL; work = g_list_find(dw->dupes, old); if (work) work->data = new; } static void dupe_match_print_group(DupeItem *di) { GList *work; printf("+ %f %s\n", di->group_rank, di->fd->name); work = di->group; while (work) { DupeMatch *dm = work->data; work = work->next; printf(" %f %s\n", dm->rank, dm->di->fd->name); } printf("\n"); } static void dupe_match_print_list(GList *list) { GList *work; work = list; while (work) { DupeItem *di = work->data; dupe_match_print_group(di); work = work->next; } } /* level 3, unlinking and orphan handling */ static GList *dupe_match_unlink_by_rank(DupeItem *child, DupeItem *parent, GList *list, DupeWindow *dw) { DupeItem *best; best = dupe_match_highest_rank(parent); if (best == child || dupe_match_highest_rank(child) == parent) { GList *work; gdouble rank; DEBUG_2("link found %s to %s [%d]", child->fd->name, parent->fd->name, g_list_length(parent->group)); work = parent->group; while (work) { DupeMatch *dm = work->data; DupeItem *orphan; work = work->next; orphan = dm->di; if (orphan != child && g_list_length(orphan->group) < 2) { dupe_match_link_clear(orphan, TRUE); if (!dw->second_set || orphan->second) { dupe_match(orphan, child, dw->match_mask, &rank, FALSE); dupe_match_link(orphan, child, rank); } list = g_list_remove(list, orphan); } } rank = dupe_match_link_rank(child, parent); dupe_match_link_clear(parent, TRUE); dupe_match_link(child, parent, rank); list = g_list_remove(list, parent); } else { DEBUG_2("unlinking %s and %s", child->fd->name, parent->fd->name); dupe_match_unlink(child, parent); } return list; } /* level 2 */ static GList *dupe_match_group_filter(GList *list, DupeItem *di, DupeWindow *dw) { GList *work; work = g_list_last(di->group); while (work) { DupeMatch *dm = work->data; work = work->prev; list = dupe_match_unlink_by_rank(di, dm->di, list, dw); } return list; } /* level 1 (top) */ static GList *dupe_match_group_trim(GList *list, DupeWindow *dw) { GList *work; work = list; while (work) { DupeItem *di = work->data; if (!di->second) list = dupe_match_group_filter(list, di, dw); work = work->next; if (di->second) list = g_list_remove(list, di); } return list; } static gint dupe_match_sort_groups_cb(gconstpointer a, gconstpointer b) { DupeMatch *da = (DupeMatch *)a; DupeMatch *db = (DupeMatch *)b; if (da->rank > db->rank) return -1; if (da->rank < db->rank) return 1; return 0; } static void dupe_match_sort_groups(GList *list) { GList *work; work = list; while (work) { DupeItem *di = work->data; di->group = g_list_sort(di->group, dupe_match_sort_groups_cb); work = work->next; } } static gint dupe_match_rank_sort_cb(gconstpointer a, gconstpointer b) { DupeItem *da = (DupeItem *)a; DupeItem *db = (DupeItem *)b; if (da->group_rank > db->group_rank) return -1; if (da->group_rank < db->group_rank) return 1; return 0; } /* returns allocated GList of dupes sorted by rank */ static GList *dupe_match_rank_sort(GList *source_list) { GList *list = NULL; GList *work; work = source_list; while (work) { DupeItem *di = work->data; if (di->group) { dupe_match_rank_update(di); list = g_list_prepend(list, di); } work = work->next; } return g_list_sort(list, dupe_match_rank_sort_cb); } static void dupe_match_rank(DupeWindow *dw) { GList *list; list = dupe_match_rank_sort(dw->list); if (required_debug_level(2)) dupe_match_print_list(list); DEBUG_1("Similar items: %d", g_list_length(list)); list = dupe_match_group_trim(list, dw); DEBUG_1("Unique groups: %d", g_list_length(list)); dupe_match_sort_groups(list); if (required_debug_level(2)) dupe_match_print_list(list); list = dupe_match_rank_sort(list); g_list_free(dw->dupes); dw->dupes = list; } /* * ------------------------------------------------------------------ * Match group tests * ------------------------------------------------------------------ */ static gint dupe_match(DupeItem *a, DupeItem *b, DupeMatchType mask, gdouble *rank, gint fast) { *rank = 0.0; if (a == b) return FALSE; if (mask & DUPE_MATCH_PATH) { if (strcmp(a->fd->path, b->fd->path) != 0) return FALSE; } if (mask & DUPE_MATCH_NAME) { if (strcmp(a->fd->name, b->fd->name) != 0) return FALSE; } if (mask & DUPE_MATCH_NAME_CI) { if (strcasecmp(a->fd->name, b->fd->name) != 0) return FALSE; } if (mask & DUPE_MATCH_SIZE) { if (a->fd->size != b->fd->size) return FALSE; } if (mask & DUPE_MATCH_DATE) { if (a->fd->date != b->fd->date) return FALSE; } if (mask & DUPE_MATCH_SUM) { if (!a->md5sum) a->md5sum = md5_text_from_file_utf8(a->fd->path, ""); if (!b->md5sum) b->md5sum = md5_text_from_file_utf8(b->fd->path, ""); if (a->md5sum[0] == '\0' || b->md5sum[0] == '\0' || strcmp(a->md5sum, b->md5sum) != 0) return FALSE; } if (mask & DUPE_MATCH_DIM) { if (a->width == 0) image_load_dimensions(a->fd, &a->width, &a->height); if (b->width == 0) image_load_dimensions(b->fd, &b->width, &b->height); if (a->width != b->width || a->height != b->height) return FALSE; } if (mask & DUPE_MATCH_SIM_HIGH || mask & DUPE_MATCH_SIM_MED || mask & DUPE_MATCH_SIM_LOW || mask & DUPE_MATCH_SIM_CUSTOM) { gdouble f; gdouble m; if (mask & DUPE_MATCH_SIM_HIGH) m = 0.95; else if (mask & DUPE_MATCH_SIM_MED) m = 0.90; else if (mask & DUPE_MATCH_SIM_CUSTOM) m = (gdouble)options->duplicates_similarity_threshold / 100.0; else m = 0.85; if (fast) { f = image_sim_compare_fast(a->simd, b->simd, m); } else { f = image_sim_compare(a->simd, b->simd); } *rank = f * 100.0; if (f < m) return FALSE; DEBUG_3("similar: %32s %32s = %f", a->fd->name, b->fd->name, f); } return TRUE; } static void dupe_list_check_match(DupeWindow *dw, DupeItem *needle, GList *start) { GList *work; if (dw->second_set) { work = dw->second_list; } else if (start) { work = start; } else { work = g_list_last(dw->list); } while (work) { DupeItem *di = work->data; /* speed opt: forward for second set, back for simple compare */ if (dw->second_set) work = work->next; else work = work->prev; if (!dupe_match_link_exists(needle, di)) { gdouble rank; if (dupe_match(di, needle, dw->match_mask, &rank, TRUE)) { dupe_match_link(di, needle, rank); } } } } /* * ------------------------------------------------------------------ * Thumbnail handling * ------------------------------------------------------------------ */ static void dupe_listview_set_thumb(DupeWindow *dw, DupeItem *di, GtkTreeIter *iter) { GtkListStore *store; GtkTreeIter iter_n; store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview))); if (!iter) { if (dupe_listview_find_item(store, di, &iter_n) >= 0) { iter = &iter_n; } } if (iter) gtk_list_store_set(store, iter, DUPE_COLUMN_THUMB, di->pixbuf, -1); } static void dupe_thumb_do(DupeWindow *dw) { DupeItem *di; if (!dw->thumb_loader || !dw->thumb_item) return; di = dw->thumb_item; if (di->pixbuf) g_object_unref(di->pixbuf); di->pixbuf = thumb_loader_get_pixbuf(dw->thumb_loader, TRUE); dupe_listview_set_thumb(dw, di, NULL); } static void dupe_thumb_error_cb(ThumbLoader *tl, gpointer data) { DupeWindow *dw = data; dupe_thumb_do(dw); dupe_thumb_step(dw); } static void dupe_thumb_done_cb(ThumbLoader *tl, gpointer data) { DupeWindow *dw = data; dupe_thumb_do(dw); dupe_thumb_step(dw); } static void dupe_thumb_step(DupeWindow *dw) { GtkTreeModel *store; GtkTreeIter iter; DupeItem *di = NULL; gint valid; gint row = 0; gint length = 0; store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)); valid = gtk_tree_model_get_iter_first(store, &iter); while (!di && valid) { GdkPixbuf *pixbuf; length++; gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, DUPE_COLUMN_THUMB, &pixbuf, -1); if (pixbuf || di->pixbuf) { if (!pixbuf) gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_THUMB, di->pixbuf, -1); row++; di = NULL; } valid = gtk_tree_model_iter_next(store, &iter); } if (valid) { while (gtk_tree_model_iter_next(store, &iter)) length++; } if (!di) { dw->thumb_item = NULL; thumb_loader_free(dw->thumb_loader); dw->thumb_loader = NULL; dupe_window_update_progress(dw, NULL, 0.0, FALSE); return; } dupe_window_update_progress(dw, _("Loading thumbs..."), length == 0 ? 0.0 : (gdouble)(row) / length, FALSE); dw->thumb_item = di; thumb_loader_free(dw->thumb_loader); dw->thumb_loader = thumb_loader_new(options->thumbnails.max_width, options->thumbnails.max_height); thumb_loader_set_callbacks(dw->thumb_loader, dupe_thumb_done_cb, dupe_thumb_error_cb, NULL, dw); /* start it */ if (!thumb_loader_start(dw->thumb_loader, di->fd->path)) { /* error, handle it, do next */ DEBUG_1("error loading thumb for %s", di->fd->path); dupe_thumb_do(dw); dupe_thumb_step(dw); } } /* * ------------------------------------------------------------------ * Dupe checking loop * ------------------------------------------------------------------ */ static void dupe_check_stop(DupeWindow *dw) { if (dw->idle_id != -1 || dw->img_loader || dw->thumb_loader) { g_source_remove(dw->idle_id); dw->idle_id = -1; dupe_window_update_progress(dw, NULL, 0.0, FALSE); widget_set_cursor(dw->listview, -1); } thumb_loader_free(dw->thumb_loader); dw->thumb_loader = NULL; image_loader_free(dw->img_loader); dw->img_loader = NULL; } static void dupe_loader_done_cb(ImageLoader *il, gpointer data) { DupeWindow *dw = data; GdkPixbuf *pixbuf; pixbuf = image_loader_get_pixbuf(il); if (dw->setup_point) { DupeItem *di = dw->setup_point->data; if (!di->simd) { di->simd = image_sim_new_from_pixbuf(pixbuf); } else { image_sim_fill_data(di->simd, pixbuf); } if (di->width == 0 && di->height == 0) { di->width = gdk_pixbuf_get_width(pixbuf); di->height = gdk_pixbuf_get_height(pixbuf); } if (options->thumbnails.enable_caching) { dupe_item_write_cache(di); } image_sim_alternate_processing(di->simd); } image_loader_free(dw->img_loader); dw->img_loader = NULL; dw->idle_id = g_idle_add(dupe_check_cb, dw); } static void dupe_setup_reset(DupeWindow *dw) { dw->setup_point = NULL; dw->setup_n = 0; dw->setup_time = msec_time(); dw->setup_time_count = 0; } static GList *dupe_setup_point_step(DupeWindow *dw, GList *p) { if (!p) return NULL; if (p->next) return p->next; if (dw->second_set && g_list_first(p) == dw->list) return dw->second_list; return NULL; } static gint dupe_check_cb(gpointer data) { DupeWindow *dw = data; if (dw->idle_id == -1) return FALSE; if (!dw->setup_done) { if ((dw->match_mask & DUPE_MATCH_SUM) && !(dw->setup_mask & DUPE_MATCH_SUM) ) { if (!dw->setup_point) dw->setup_point = dw->list; while (dw->setup_point) { DupeItem *di = dw->setup_point->data; dw->setup_point = dupe_setup_point_step(dw, dw->setup_point); dw->setup_n++; if (!di->md5sum) { dupe_window_update_progress(dw, _("Reading checksums..."), dw->setup_count == 0 ? 0.0 : (gdouble)(dw->setup_n - 1) / dw->setup_count, FALSE); if (options->thumbnails.enable_caching) { dupe_item_read_cache(di); if (di->md5sum) return TRUE; } di->md5sum = md5_text_from_file_utf8(di->fd->path, ""); if (options->thumbnails.enable_caching) { dupe_item_write_cache(di); } return TRUE; } } dw->setup_mask |= DUPE_MATCH_SUM; dupe_setup_reset(dw); } if ((dw->match_mask & DUPE_MATCH_DIM) && !(dw->setup_mask & DUPE_MATCH_DIM) ) { if (!dw->setup_point) dw->setup_point = dw->list; while (dw->setup_point) { DupeItem *di = dw->setup_point->data; dw->setup_point = dupe_setup_point_step(dw, dw->setup_point); dw->setup_n++; if (di->width == 0 && di->height == 0) { dupe_window_update_progress(dw, _("Reading dimensions..."), dw->setup_count == 0 ? 0.0 : (gdouble)(dw->setup_n - 1) / dw->setup_count, FALSE); if (options->thumbnails.enable_caching) { dupe_item_read_cache(di); if (di->width != 0 || di->height != 0) return TRUE; } image_load_dimensions(di->fd, &di->width, &di->height); if (options->thumbnails.enable_caching) { dupe_item_write_cache(di); } return TRUE; } } dw->setup_mask |= DUPE_MATCH_DIM; dupe_setup_reset(dw); } if ((dw->match_mask & DUPE_MATCH_SIM_HIGH || dw->match_mask & DUPE_MATCH_SIM_MED || dw->match_mask & DUPE_MATCH_SIM_LOW || dw->match_mask & DUPE_MATCH_SIM_CUSTOM) && !(dw->setup_mask & DUPE_MATCH_SIM_MED) ) { if (!dw->setup_point) dw->setup_point = dw->list; while (dw->setup_point) { DupeItem *di = dw->setup_point->data; if (!di->simd) { dupe_window_update_progress(dw, _("Reading similarity data..."), dw->setup_count == 0 ? 0.0 : (gdouble)dw->setup_n / dw->setup_count, FALSE); if (options->thumbnails.enable_caching) { dupe_item_read_cache(di); if (cache_sim_data_filled(di->simd)) { image_sim_alternate_processing(di->simd); return TRUE; } } dw->img_loader = image_loader_new(di->fd); image_loader_set_buffer_size(dw->img_loader, 8); image_loader_set_error_func(dw->img_loader, dupe_loader_done_cb, dw); if (!image_loader_start(dw->img_loader, dupe_loader_done_cb, dw)) { image_sim_free(di->simd); di->simd = image_sim_new(); image_loader_free(dw->img_loader); dw->img_loader = NULL; return TRUE; } dw->idle_id = -1; return FALSE; } dw->setup_point = dupe_setup_point_step(dw, dw->setup_point); dw->setup_n++; } dw->setup_mask |= DUPE_MATCH_SIM_MED; dupe_setup_reset(dw); } dupe_window_update_progress(dw, _("Comparing..."), 0.0, FALSE); dw->setup_done = TRUE; dupe_setup_reset(dw); dw->setup_count = g_list_length(dw->list); } if (!dw->working) { if (dw->setup_count > 0) { dw->setup_count = 0; dupe_window_update_progress(dw, _("Sorting..."), 1.0, TRUE); return TRUE; } dw->idle_id = -1; dupe_window_update_progress(dw, NULL, 0.0, FALSE); dupe_match_rank(dw); dupe_window_update_count(dw, FALSE); dupe_listview_populate(dw); /* check thumbs */ if (dw->show_thumbs) dupe_thumb_step(dw); widget_set_cursor(dw->listview, -1); return FALSE; } dupe_list_check_match(dw, (DupeItem *)dw->working->data, dw->working); dupe_window_update_progress(dw, _("Comparing..."), dw->setup_count == 0 ? 0.0 : (gdouble) dw->setup_n / dw->setup_count, FALSE); dw->setup_n++; dw->working = dw->working->prev; return TRUE; } static void dupe_check_start(DupeWindow *dw) { dw->setup_done = FALSE; dw->setup_count = g_list_length(dw->list); if (dw->second_set) dw->setup_count += g_list_length(dw->second_list); dw->setup_mask = 0; dupe_setup_reset(dw); dw->working = g_list_last(dw->list); dupe_window_update_count(dw, TRUE); widget_set_cursor(dw->listview, GDK_WATCH); if (dw->idle_id != -1) return; dw->idle_id = g_idle_add(dupe_check_cb, dw); } /* * ------------------------------------------------------------------ * Item addition, removal * ------------------------------------------------------------------ */ static void dupe_item_remove(DupeWindow *dw, DupeItem *di) { if (!di) return; /* handle things that may be in progress... */ if (dw->working && dw->working->data == di) { dw->working = dw->working->prev; } if (dw->thumb_loader && dw->thumb_item == di) { dupe_thumb_step(dw); } if (dw->setup_point && dw->setup_point->data == di) { dw->setup_point = dupe_setup_point_step(dw, dw->setup_point); if (dw->img_loader) { image_loader_free(dw->img_loader); dw->img_loader = NULL; dw->idle_id = g_idle_add(dupe_check_cb, dw); } } if (di->group && dw->dupes) { /* is a dupe, must remove from group/reset children if a parent */ DupeItem *parent; parent = dupe_match_find_parent(dw, di); if (di == parent) { if (g_list_length(parent->group) < 2) { DupeItem *child; child = dupe_match_highest_rank(parent); dupe_match_link_clear(child, TRUE); dupe_listview_remove(dw, child); dupe_match_link_clear(parent, TRUE); dupe_listview_remove(dw, parent); dw->dupes = g_list_remove(dw->dupes, parent); } else { DupeItem *new_parent; DupeMatch *dm; dm = parent->group->data; new_parent = dm->di; dupe_match_reparent(dw, parent, new_parent); dupe_listview_remove(dw, parent); } } else { if (g_list_length(parent->group) < 2) { dupe_match_link_clear(parent, TRUE); dupe_listview_remove(dw, parent); dw->dupes = g_list_remove(dw->dupes, parent); } dupe_match_link_clear(di, TRUE); dupe_listview_remove(dw, di); } } else { /* not a dupe, or not sorted yet, simply reset */ dupe_match_link_clear(di, TRUE); } if (dw->second_list && g_list_find(dw->second_list, di)) { dupe_second_remove(dw, di); } else { dw->list = g_list_remove(dw->list, di); } dupe_item_free(di); dupe_window_update_count(dw, FALSE); } static gint dupe_item_remove_by_path(DupeWindow *dw, const gchar *path) { DupeItem *di; di = dupe_item_find_path(dw, path); if (!di) return FALSE; dupe_item_remove(dw, di); return TRUE; } static void dupe_files_add(DupeWindow *dw, CollectionData *collection, CollectInfo *info, FileData *fd, gint recurse) { DupeItem *di = NULL; if (info) { di = dupe_item_new(info->fd); } else if (fd) { if (isfile(fd->path)) { di = dupe_item_new(fd); } else if (isdir(fd->path) && recurse) { GList *f, *d; if (filelist_read(fd->path, &f, &d)) { GList *work; f = filelist_filter(f, FALSE); d = filelist_filter(d, TRUE); work = f; while (work) { dupe_files_add(dw, NULL, NULL, (FileData *)work->data, TRUE); work = work->next; } filelist_free(f); work = d; while (work) { dupe_files_add(dw, NULL, NULL, (FileData *)work->data, TRUE); work = work->next; } filelist_free(d); } } } if (!di) return; if (dw->second_drop) { dupe_second_add(dw, di); } else { dw->list = g_list_prepend(dw->list, di); } } void dupe_window_add_collection(DupeWindow *dw, CollectionData *collection) { CollectInfo *info; info = collection_get_first(collection); while (info) { dupe_files_add(dw, collection, info, NULL, FALSE); info = collection_next_by_info(collection, info); } dupe_check_start(dw); } void dupe_window_add_files(DupeWindow *dw, GList *list, gint recurse) { GList *work; work = list; while (work) { FileData *fd = work->data; work = work->next; dupe_files_add(dw, NULL, NULL, fd, recurse); } dupe_check_start(dw); } static void dupe_item_update(DupeWindow *dw, DupeItem *di) { if ( (dw->match_mask & DUPE_MATCH_NAME) || (dw->match_mask & DUPE_MATCH_PATH || (dw->match_mask & DUPE_MATCH_NAME_CI)) ) { /* only effects matches on name or path */ /* FileData *fd = file_data_ref(di->fd); gint second; second = di->second; dupe_item_remove(dw, di); dw->second_drop = second; dupe_files_add(dw, NULL, NULL, fd, FALSE); dw->second_drop = FALSE; file_data_unref(fd); */ dupe_check_start(dw); } else { GtkListStore *store; GtkTreeIter iter; gint row; /* update the listview(s) */ store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview))); row = dupe_listview_find_item(store, di, &iter); if (row >= 0) { gtk_list_store_set(store, &iter, DUPE_COLUMN_NAME, di->fd->name, DUPE_COLUMN_PATH, di->fd->path, -1); } if (dw->second_listview) { store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview))); row = dupe_listview_find_item(store, di, &iter); if (row >= 0) { gtk_list_store_set(store, &iter, 1, di->fd->path, -1); } } } } static void dupe_item_update_fd_in_list(DupeWindow *dw, FileData *fd, GList *work) { while (work) { DupeItem *di = work->data; if (di->fd == fd) dupe_item_update(dw, di); work = work->next; } } static void dupe_item_update_fd(DupeWindow *dw, FileData *fd) { dupe_item_update_fd_in_list(dw, fd, dw->list); if (dw->second_set) dupe_item_update_fd_in_list(dw, fd, dw->second_list); } /* * ------------------------------------------------------------------ * Misc. * ------------------------------------------------------------------ */ static GtkWidget *dupe_display_label(GtkWidget *vbox, const gchar *description, const gchar *text) { GtkWidget *hbox; GtkWidget *label; hbox = gtk_hbox_new(FALSE, 10); label = gtk_label_new(description); gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0); gtk_widget_show(label); label = gtk_label_new(text); gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0); gtk_widget_show(label); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); gtk_widget_show(hbox); return label; } static void dupe_display_stats(DupeWindow *dw, DupeItem *di) { GenericDialog *gd; gchar *buf; if (!di) return; gd = file_util_gen_dlg("Image thumbprint debug info", GQ_WMCLASS, "thumbprint", dw->window, TRUE, NULL, NULL); generic_dialog_add_button(gd, GTK_STOCK_CLOSE, NULL, NULL, TRUE); dupe_display_label(gd->vbox, "name:", di->fd->name); buf = text_from_size(di->fd->size); dupe_display_label(gd->vbox, "size:", buf); g_free(buf); dupe_display_label(gd->vbox, "date:", text_from_time(di->fd->date)); buf = g_strdup_printf("%d x %d", di->width, di->height); dupe_display_label(gd->vbox, "dimensions:", buf); g_free(buf); dupe_display_label(gd->vbox, "md5sum:", (di->md5sum) ? di->md5sum : "not generated"); dupe_display_label(gd->vbox, "thumbprint:", (di->simd) ? "" : "not generated"); if (di->simd) { GtkWidget *image; GdkPixbuf *pixbuf; gint x, y; guchar *d_pix; guchar *dp; gint rs; gint sp; pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, 32, 32); rs = gdk_pixbuf_get_rowstride(pixbuf); d_pix = gdk_pixbuf_get_pixels(pixbuf); for (y = 0; y < 32; y++) { dp = d_pix + (y * rs); sp = y * 32; for (x = 0; x < 32; x++) { *(dp++) = di->simd->avg_r[sp + x]; *(dp++) = di->simd->avg_g[sp + x]; *(dp++) = di->simd->avg_b[sp + x]; } } image = gtk_image_new_from_pixbuf(pixbuf); gtk_box_pack_start(GTK_BOX(gd->vbox), image, FALSE, FALSE, 0); gtk_widget_show(image); gdk_pixbuf_unref(pixbuf); } gtk_widget_show(gd->dialog); } static void dupe_window_recompare(DupeWindow *dw) { GtkListStore *store; dupe_check_stop(dw); store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview))); gtk_list_store_clear(store); g_list_free(dw->dupes); dw->dupes = NULL; dupe_match_reset_list(dw->list); dupe_match_reset_list(dw->second_list); dupe_check_start(dw); } static void dupe_menu_view(DupeWindow *dw, DupeItem *di, GtkWidget *listview, gint new_window) { if (!di) return; if (di->collection && collection_info_valid(di->collection, di->info)) { if (new_window) { view_window_new_from_collection(di->collection, di->info); } else { layout_image_set_collection(NULL, di->collection, di->info); } } else { if (new_window) { GList *list; list = dupe_listview_get_selection(dw, listview); view_window_new_from_list(list); filelist_free(list); } else { layout_image_set_fd(NULL, di->fd); } } } static void dupe_window_remove_selection(DupeWindow *dw, GtkWidget *listview) { GtkTreeSelection *selection; GtkTreeModel *store; GtkTreeIter iter; GList *slist; GList *list = NULL; GList *work; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview)); slist = gtk_tree_selection_get_selected_rows(selection, &store); work = slist; while (work) { GtkTreePath *tpath = work->data; DupeItem *di = NULL; gtk_tree_model_get_iter(store, &iter, tpath); gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1); if (di) list = g_list_prepend(list, di); work = work->next; } g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL); g_list_free(slist); dw->color_frozen = TRUE; work = list; while (work) { DupeItem *di; di = work->data; work = work->next; dupe_item_remove(dw, di); } dw->color_frozen = FALSE; g_list_free(list); dupe_listview_realign_colors(dw); } static void dupe_window_edit_selected(DupeWindow *dw, gint n) { GList *list; list = dupe_listview_get_selection(dw, dw->listview); start_editor_from_filelist(n, list); filelist_free(list); } static void dupe_window_collection_from_selection(DupeWindow *dw) { CollectWindow *w; GList *list; list = dupe_listview_get_selection(dw, dw->listview); w = collection_window_new(NULL); collection_table_add_filelist(w->table, list); filelist_free(list); } static void dupe_window_append_file_list(DupeWindow *dw, gint on_second) { GList *list; dw->second_drop = (dw->second_set && on_second); list = layout_list(NULL); dupe_window_add_files(dw, list, FALSE); filelist_free(list); } /* *------------------------------------------------------------------- * main pop-up menu callbacks *------------------------------------------------------------------- */ static void dupe_menu_view_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->listview, FALSE); } static void dupe_menu_viewnew_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->listview, TRUE); } static void dupe_menu_select_all_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; GtkTreeSelection *selection; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview)); gtk_tree_selection_select_all(selection); } static void dupe_menu_select_none_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; GtkTreeSelection *selection; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview)); gtk_tree_selection_unselect_all(selection); } static void dupe_menu_select_dupes_set1_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; dupe_listview_select_dupes(dw, TRUE); } static void dupe_menu_select_dupes_set2_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; dupe_listview_select_dupes(dw, FALSE); } static void dupe_menu_edit_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw; gint n; dw = submenu_item_get_data(widget); n = GPOINTER_TO_INT(data); if (!dw) return; dupe_window_edit_selected(dw, n); } static void dupe_menu_info_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; info_window_new(NULL, dupe_listview_get_selection(dw, dw->listview), NULL); } static void dupe_menu_collection_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; dupe_window_collection_from_selection(dw); } static void dupe_menu_print_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; FileData *fd; fd = (dw->click_item) ? dw->click_item->fd : NULL; print_window_new(fd, dupe_listview_get_selection(dw, dw->listview), dupe_listview_get_filelist(dw, dw->listview), dw->window); } static void dupe_menu_copy_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; file_util_copy(NULL, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window); } static void dupe_menu_move_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; file_util_move(NULL, dupe_listview_get_selection(dw, dw->listview), NULL, dw->window); } static void dupe_menu_rename_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; file_util_rename(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window); } static void dupe_menu_delete_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; file_util_delete(NULL, dupe_listview_get_selection(dw, dw->listview), dw->window); } static void dupe_menu_copy_path_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; file_util_copy_path_list_to_clipboard(dupe_listview_get_selection(dw, dw->listview)); } static void dupe_menu_remove_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; dupe_window_remove_selection(dw, dw->listview); } static void dupe_menu_clear_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; dupe_window_clear(dw); } static void dupe_menu_close_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; dupe_window_close(dw); } static GtkWidget *dupe_menu_popup_main(DupeWindow *dw, DupeItem *di) { GtkWidget *menu; GtkWidget *item; gint on_row; on_row = (di != NULL); menu = popup_menu_short_lived(); menu_item_add_sensitive(menu, _("_View"), on_row, G_CALLBACK(dupe_menu_view_cb), dw); menu_item_add_stock_sensitive(menu, _("View in _new window"), GTK_STOCK_NEW, on_row, G_CALLBACK(dupe_menu_viewnew_cb), dw); menu_item_add_divider(menu); menu_item_add_sensitive(menu, _("Select all"), (dw->dupes != NULL), G_CALLBACK(dupe_menu_select_all_cb), dw); menu_item_add_sensitive(menu, _("Select none"), (dw->dupes != NULL), G_CALLBACK(dupe_menu_select_none_cb), dw); menu_item_add_sensitive(menu, _("Select group _1 duplicates"), (dw->dupes != NULL), G_CALLBACK(dupe_menu_select_dupes_set1_cb), dw); menu_item_add_sensitive(menu, _("Select group _2 duplicates"), (dw->dupes != NULL), G_CALLBACK(dupe_menu_select_dupes_set2_cb), dw); menu_item_add_divider(menu); submenu_add_edit(menu, &item, G_CALLBACK(dupe_menu_edit_cb), dw); if (!on_row) gtk_widget_set_sensitive(item, FALSE); menu_item_add_stock_sensitive(menu, _("_Properties"), GTK_STOCK_PROPERTIES, on_row, G_CALLBACK(dupe_menu_info_cb), dw); menu_item_add_stock_sensitive(menu, _("Add to new collection"), GTK_STOCK_INDEX, on_row, G_CALLBACK(dupe_menu_collection_cb), dw); menu_item_add_stock_sensitive(menu, _("Print..."), GTK_STOCK_PRINT, on_row, G_CALLBACK(dupe_menu_print_cb), dw); menu_item_add_divider(menu); menu_item_add_stock_sensitive(menu, _("_Copy..."), GTK_STOCK_COPY, on_row, G_CALLBACK(dupe_menu_copy_cb), dw); menu_item_add_sensitive(menu, _("_Move..."), on_row, G_CALLBACK(dupe_menu_move_cb), dw); menu_item_add_sensitive(menu, _("_Rename..."), on_row, G_CALLBACK(dupe_menu_rename_cb), dw); menu_item_add_stock_sensitive(menu, _("_Delete..."), GTK_STOCK_DELETE, on_row, G_CALLBACK(dupe_menu_delete_cb), dw); if (options->show_copy_path) menu_item_add_sensitive(menu, _("_Copy path"), on_row, G_CALLBACK(dupe_menu_copy_path_cb), dw); menu_item_add_divider(menu); menu_item_add_stock_sensitive(menu, _("Rem_ove"), GTK_STOCK_REMOVE, on_row, G_CALLBACK(dupe_menu_remove_cb), dw); menu_item_add_stock_sensitive(menu, _("C_lear"), GTK_STOCK_CLEAR, (dw->list != NULL), G_CALLBACK(dupe_menu_clear_cb), dw); menu_item_add_divider(menu); menu_item_add_stock(menu, _("Close _window"), GTK_STOCK_CLOSE, G_CALLBACK(dupe_menu_close_cb), dw); return menu; } static gint dupe_listview_press_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data) { DupeWindow *dw = data; GtkTreeModel *store; GtkTreePath *tpath; GtkTreeIter iter; DupeItem *di = NULL; store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget)); if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y, &tpath, NULL, NULL, NULL)) { gtk_tree_model_get_iter(store, &iter, tpath); gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1); gtk_tree_path_free(tpath); } dw->click_item = di; if (bevent->button == MOUSE_BUTTON_RIGHT) { /* right click menu */ GtkWidget *menu; if (bevent->state & GDK_CONTROL_MASK && bevent->state & GDK_SHIFT_MASK) { dupe_display_stats(dw, di); return TRUE; } if (widget == dw->listview) { menu = dupe_menu_popup_main(dw, di); } else { menu = dupe_menu_popup_second(dw, di); } gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, bevent->button, bevent->time); } if (!di) return FALSE; if (bevent->button == MOUSE_BUTTON_LEFT && bevent->type == GDK_2BUTTON_PRESS) { dupe_menu_view(dw, di, widget, FALSE); } if (bevent->button == MOUSE_BUTTON_MIDDLE) return TRUE; if (bevent->button == MOUSE_BUTTON_RIGHT) { if (!dupe_listview_item_is_selected(dw, di, widget)) { GtkTreeSelection *selection; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget)); gtk_tree_selection_unselect_all(selection); gtk_tree_selection_select_iter(selection, &iter); tpath = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter); gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE); gtk_tree_path_free(tpath); } return TRUE; } if (bevent->button == MOUSE_BUTTON_LEFT && bevent->type == GDK_BUTTON_PRESS && !(bevent->state & GDK_SHIFT_MASK ) && !(bevent->state & GDK_CONTROL_MASK ) && dupe_listview_item_is_selected(dw, di, widget)) { /* this selection handled on release_cb */ gtk_widget_grab_focus(widget); return TRUE; } return FALSE; } static gint dupe_listview_release_cb(GtkWidget *widget, GdkEventButton *bevent, gpointer data) { DupeWindow *dw = data; GtkTreeModel *store; GtkTreePath *tpath; GtkTreeIter iter; DupeItem *di = NULL; if (bevent->button != MOUSE_BUTTON_LEFT && bevent->button != MOUSE_BUTTON_MIDDLE) return TRUE; store = gtk_tree_view_get_model(GTK_TREE_VIEW(widget)); if ((bevent->x != 0 || bevent->y != 0) && gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(widget), bevent->x, bevent->y, &tpath, NULL, NULL, NULL)) { gtk_tree_model_get_iter(store, &iter, tpath); gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1); gtk_tree_path_free(tpath); } if (bevent->button == MOUSE_BUTTON_MIDDLE) { if (di && dw->click_item == di) { GtkTreeSelection *selection; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget)); if (dupe_listview_item_is_selected(dw, di, widget)) { gtk_tree_selection_unselect_iter(selection, &iter); } else { gtk_tree_selection_select_iter(selection, &iter); } } return TRUE; } if (di && dw->click_item == di && !(bevent->state & GDK_SHIFT_MASK ) && !(bevent->state & GDK_CONTROL_MASK ) && dupe_listview_item_is_selected(dw, di, widget)) { GtkTreeSelection *selection; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget)); gtk_tree_selection_unselect_all(selection); gtk_tree_selection_select_iter(selection, &iter); tpath = gtk_tree_model_get_path(store, &iter); gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE); gtk_tree_path_free(tpath); return TRUE; } return FALSE; } /* *------------------------------------------------------------------- * second set stuff *------------------------------------------------------------------- */ static void dupe_second_update_status(DupeWindow *dw) { gchar *buf; buf = g_strdup_printf(_("%d files (set 2)"), g_list_length(dw->second_list)); gtk_label_set_text(GTK_LABEL(dw->second_status_label), buf); g_free(buf); } static void dupe_second_add(DupeWindow *dw, DupeItem *di) { GtkListStore *store; GtkTreeIter iter; if (!di) return; di->second = TRUE; dw->second_list = g_list_prepend(dw->second_list, di); store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview))); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, DUPE_COLUMN_POINTER, di, 1, di->fd->path, -1); dupe_second_update_status(dw); } static void dupe_second_remove(DupeWindow *dw, DupeItem *di) { GtkListStore *store; GtkTreeIter iter; store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview))); if (dupe_listview_find_item(store, di, &iter) >= 0) { tree_view_move_cursor_away(GTK_TREE_VIEW(dw->second_listview), &iter, TRUE); gtk_list_store_remove(store, &iter); } dw->second_list = g_list_remove(dw->second_list, di); dupe_second_update_status(dw); } static void dupe_second_clear(DupeWindow *dw) { GtkListStore *store; store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->second_listview))); gtk_list_store_clear(store); gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->second_listview)); g_list_free(dw->dupes); dw->dupes = NULL; dupe_list_free(dw->second_list); dw->second_list = NULL; dupe_match_reset_list(dw->list); dupe_second_update_status(dw); } static void dupe_second_menu_view_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->second_listview, FALSE); } static void dupe_second_menu_viewnew_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; if (dw->click_item) dupe_menu_view(dw, dw->click_item, dw->second_listview, TRUE); } static void dupe_second_menu_select_all_cb(GtkWidget *widget, gpointer data) { GtkTreeSelection *selection; DupeWindow *dw = data; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview)); gtk_tree_selection_select_all(selection); } static void dupe_second_menu_select_none_cb(GtkWidget *widget, gpointer data) { GtkTreeSelection *selection; DupeWindow *dw = data; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview)); gtk_tree_selection_unselect_all(selection); } static void dupe_second_menu_remove_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; dupe_window_remove_selection(dw, dw->second_listview); } static void dupe_second_menu_clear_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; dupe_second_clear(dw); dupe_window_recompare(dw); } static GtkWidget *dupe_menu_popup_second(DupeWindow *dw, DupeItem *di) { GtkWidget *menu; gint notempty; gint on_row; on_row = (di != NULL); notempty = (dw->second_list != NULL); menu = popup_menu_short_lived(); menu_item_add_sensitive(menu, _("_View"), on_row, G_CALLBACK(dupe_second_menu_view_cb), dw); menu_item_add_stock_sensitive(menu, _("View in _new window"), GTK_STOCK_NEW, on_row, G_CALLBACK(dupe_second_menu_viewnew_cb), dw); menu_item_add_divider(menu); menu_item_add_sensitive(menu, _("Select all"), notempty, G_CALLBACK(dupe_second_menu_select_all_cb), dw); menu_item_add_sensitive(menu, _("Select none"), notempty, G_CALLBACK(dupe_second_menu_select_none_cb), dw); menu_item_add_divider(menu); menu_item_add_stock_sensitive(menu, _("Rem_ove"), GTK_STOCK_REMOVE, on_row, G_CALLBACK(dupe_second_menu_remove_cb), dw); menu_item_add_stock_sensitive(menu, _("C_lear"), GTK_STOCK_CLEAR, notempty, G_CALLBACK(dupe_second_menu_clear_cb), dw); menu_item_add_divider(menu); menu_item_add_stock(menu, _("Close _window"), GTK_STOCK_CLOSE, G_CALLBACK(dupe_menu_close_cb), dw); return menu; } static void dupe_second_set_toggle_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; dw->second_set = GTK_TOGGLE_BUTTON(widget)->active; if (dw->second_set) { dupe_second_update_status(dw); gtk_table_set_col_spacings(GTK_TABLE(dw->table), PREF_PAD_GAP); gtk_widget_show(dw->second_vbox); } else { gtk_table_set_col_spacings(GTK_TABLE(dw->table), 0); gtk_widget_hide(dw->second_vbox); dupe_second_clear(dw); } dupe_window_recompare(dw); } /* *------------------------------------------------------------------- * match type menu *------------------------------------------------------------------- */ enum { DUPE_MENU_COLUMN_NAME = 0, DUPE_MENU_COLUMN_MASK }; static void dupe_menu_type_cb(GtkWidget *combo, gpointer data) { DupeWindow *dw = data; GtkTreeModel *store; GtkTreeIter iter; store = gtk_combo_box_get_model(GTK_COMBO_BOX(combo)); if (!gtk_combo_box_get_active_iter(GTK_COMBO_BOX(combo), &iter)) return; gtk_tree_model_get(store, &iter, DUPE_MENU_COLUMN_MASK, &dw->match_mask, -1); dupe_window_recompare(dw); } static void dupe_menu_add_item(GtkListStore *store, const gchar *text, DupeMatchType type, DupeWindow *dw) { GtkTreeIter iter; gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, DUPE_MENU_COLUMN_NAME, text, DUPE_MENU_COLUMN_MASK, type, -1); if (dw->match_mask == type) gtk_combo_box_set_active_iter(GTK_COMBO_BOX(dw->combo), &iter); } static void dupe_menu_setup(DupeWindow *dw) { GtkListStore *store; GtkCellRenderer *renderer; store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT); dw->combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store)); g_object_unref(store); renderer = gtk_cell_renderer_text_new(); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(dw->combo), renderer, TRUE); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(dw->combo), renderer, "text", DUPE_MENU_COLUMN_NAME, NULL); dupe_menu_add_item(store, _("Name"), DUPE_MATCH_NAME, dw); dupe_menu_add_item(store, _("Name case-insensitive"), DUPE_MATCH_NAME_CI, dw); dupe_menu_add_item(store, _("Size"), DUPE_MATCH_SIZE, dw); dupe_menu_add_item(store, _("Date"), DUPE_MATCH_DATE, dw); dupe_menu_add_item(store, _("Dimensions"), DUPE_MATCH_DIM, dw); dupe_menu_add_item(store, _("Checksum"), DUPE_MATCH_SUM, dw); dupe_menu_add_item(store, _("Path"), DUPE_MATCH_PATH, dw); dupe_menu_add_item(store, _("Similarity (high)"), DUPE_MATCH_SIM_HIGH, dw); dupe_menu_add_item(store, _("Similarity"), DUPE_MATCH_SIM_MED, dw); dupe_menu_add_item(store, _("Similarity (low)"), DUPE_MATCH_SIM_LOW, dw); dupe_menu_add_item(store, _("Similarity (custom)"), DUPE_MATCH_SIM_CUSTOM, dw); g_signal_connect(G_OBJECT(dw->combo), "changed", G_CALLBACK(dupe_menu_type_cb), dw); } /* *------------------------------------------------------------------- * list view columns *------------------------------------------------------------------- */ /* this overrides the low default of a GtkCellRenderer from 100 to CELL_HEIGHT_OVERRIDE, something sane for our purposes */ #define CELL_HEIGHT_OVERRIDE 512 void cell_renderer_height_override(GtkCellRenderer *renderer) { GParamSpec *spec; spec = g_object_class_find_property(G_OBJECT_GET_CLASS(G_OBJECT(renderer)), "height"); if (spec && G_IS_PARAM_SPEC_INT(spec)) { GParamSpecInt *spec_int; spec_int = G_PARAM_SPEC_INT(spec); if (spec_int->maximum < CELL_HEIGHT_OVERRIDE) spec_int->maximum = CELL_HEIGHT_OVERRIDE; } } static GdkColor *dupe_listview_color_shifted(GtkWidget *widget) { static GdkColor color; static GtkWidget *done = NULL; if (done != widget) { GtkStyle *style; style = gtk_widget_get_style(widget); memcpy(&color, &style->base[GTK_STATE_NORMAL], sizeof(color)); shift_color(&color, -1, 0); done = widget; } return &color; } static void dupe_listview_color_cb(GtkTreeViewColumn *tree_column, GtkCellRenderer *cell, GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data) { DupeWindow *dw = data; gboolean set; gtk_tree_model_get(tree_model, iter, DUPE_COLUMN_COLOR, &set, -1); g_object_set(G_OBJECT(cell), "cell-background-gdk", dupe_listview_color_shifted(dw->listview), "cell-background-set", set, NULL); } static void dupe_listview_add_column(DupeWindow *dw, GtkWidget *listview, gint n, const gchar *title, gint image, gint right_justify) { GtkTreeViewColumn *column; GtkCellRenderer *renderer; column = gtk_tree_view_column_new(); gtk_tree_view_column_set_title(column, title); gtk_tree_view_column_set_min_width(column, 4); if (n != DUPE_COLUMN_RANK && n != DUPE_COLUMN_THUMB) { gtk_tree_view_column_set_resizable(column, TRUE); } if (!image) { gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_GROW_ONLY); renderer = gtk_cell_renderer_text_new(); if (right_justify) { g_object_set(G_OBJECT(renderer), "xalign", 1.0, NULL); } gtk_tree_view_column_pack_start(column, renderer, TRUE); gtk_tree_view_column_add_attribute(column, renderer, "text", n); } else { gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED); renderer = gtk_cell_renderer_pixbuf_new(); cell_renderer_height_override(renderer); gtk_tree_view_column_pack_start(column, renderer, TRUE); gtk_tree_view_column_add_attribute(column, renderer, "pixbuf", n); } if (listview == dw->listview) { /* sets background before rendering */ gtk_tree_view_column_set_cell_data_func(column, renderer, dupe_listview_color_cb, dw, NULL); } gtk_tree_view_append_column(GTK_TREE_VIEW(listview), column); } static void dupe_listview_set_height(GtkWidget *listview, gint thumb) { GtkTreeViewColumn *column; GtkCellRenderer *cell; GList *list; column = gtk_tree_view_get_column(GTK_TREE_VIEW(listview), DUPE_COLUMN_THUMB - 1); if (!column) return; gtk_tree_view_column_set_fixed_width(column, (thumb) ? options->thumbnails.max_width : 4); list = gtk_tree_view_column_get_cell_renderers(column); if (!list) return; cell = list->data; g_list_free(list); g_object_set(G_OBJECT(cell), "height", (thumb) ? options->thumbnails.max_height : -1, NULL); gtk_tree_view_columns_autosize(GTK_TREE_VIEW(listview)); } /* *------------------------------------------------------------------- * misc cb *------------------------------------------------------------------- */ static void dupe_window_show_thumb_cb(GtkWidget *widget, gpointer data) { DupeWindow *dw = data; dw->show_thumbs = GTK_TOGGLE_BUTTON(widget)->active; if (dw->show_thumbs) { if (!dw->working) dupe_thumb_step(dw); } else { GtkTreeModel *store; GtkTreeIter iter; gint valid; thumb_loader_free(dw->thumb_loader); dw->thumb_loader = NULL; store = gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview)); valid = gtk_tree_model_get_iter_first(store, &iter); while (valid) { gtk_list_store_set(GTK_LIST_STORE(store), &iter, DUPE_COLUMN_THUMB, NULL, -1); valid = gtk_tree_model_iter_next(store, &iter); } dupe_window_update_progress(dw, NULL, 0.0, FALSE); } dupe_listview_set_height(dw->listview, dw->show_thumbs); } static void dupe_popup_menu_pos_cb(GtkMenu *menu, gint *x, gint *y, gboolean *push_in, gpointer data) { GtkWidget *view = data; GtkTreePath *tpath; gint cx, cy, cw, ch; gint column; gtk_tree_view_get_cursor(GTK_TREE_VIEW(view), &tpath, NULL); if (!tpath) return; if (gtk_tree_view_get_column(GTK_TREE_VIEW(view), DUPE_COLUMN_NAME - 1) != NULL) { column = DUPE_COLUMN_NAME - 1; } else { /* dw->second_listview */ column = 0; } tree_view_get_cell_clamped(GTK_TREE_VIEW(view), tpath, column, TRUE, &cx, &cy, &cw, &ch); gtk_tree_path_free(tpath); cy += ch; popup_menu_position_clamp(menu, &cx, &cy, 0); *x = cx; *y = cy; } static gint dupe_window_keypress_cb(GtkWidget *widget, GdkEventKey *event, gpointer data) { DupeWindow *dw = data; gint stop_signal = FALSE; gint on_second; GtkWidget *listview; GtkTreeModel *store; GtkTreeSelection *selection; GList *slist; DupeItem *di = NULL; on_second = GTK_WIDGET_HAS_FOCUS(dw->second_listview); if (on_second) { listview = dw->second_listview; } else { listview = dw->listview; } selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(listview)); slist = gtk_tree_selection_get_selected_rows(selection, &store); if (slist) { GtkTreePath *tpath; GtkTreeIter iter; GList *last; last = g_list_last(slist); tpath = last->data; /* last is newest selected file */ gtk_tree_model_get_iter(store, &iter, tpath); gtk_tree_model_get(store, &iter, DUPE_COLUMN_POINTER, &di, -1); } g_list_foreach(slist, (GFunc)gtk_tree_path_free, NULL); g_list_free(slist); if (event->state & GDK_CONTROL_MASK) { gint edit_val = -1; if (!on_second) { stop_signal = TRUE; switch (event->keyval) { case '1': edit_val = 0; break; case '2': edit_val = 1; break; case '3': edit_val = 2; break; case '4': edit_val = 3; break; case '5': edit_val = 4; break; case '6': edit_val = 5; break; case '7': edit_val = 6; break; case '8': edit_val = 7; break; case '9': edit_val = 8; break; case '0': edit_val = 9; break; case 'C': case 'c': file_util_copy(NULL, dupe_listview_get_selection(dw, listview), NULL, dw->window); break; case 'M': case 'm': file_util_move(NULL, dupe_listview_get_selection(dw, listview), NULL, dw->window); break; case 'R': case 'r': file_util_rename(NULL, dupe_listview_get_selection(dw, listview), dw->window); break; case 'D': case 'd': file_util_delete(NULL, dupe_listview_get_selection(dw, listview), dw->window); break; case 'P': case 'p': info_window_new(NULL, dupe_listview_get_selection(dw, listview), NULL); break; default: stop_signal = FALSE; break; } } if (!stop_signal) { stop_signal = TRUE; switch (event->keyval) { case 'A': case 'a': if (event->state & GDK_SHIFT_MASK) { gtk_tree_selection_unselect_all(selection); } else { gtk_tree_selection_select_all(selection); } break; case GDK_Delete: case GDK_KP_Delete: if (on_second) { dupe_second_clear(dw); dupe_window_recompare(dw); } else { dupe_window_clear(dw); } break; case 'L': case 'l': dupe_window_append_file_list(dw, FALSE); break; case 'T': case 't': gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dw->button_thumbs), !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(dw->button_thumbs))); break; case 'W': case 'w': dupe_window_close(dw); break; default: stop_signal = FALSE; break; } } if (edit_val >= 0) { dupe_window_edit_selected(dw, edit_val); } } else { stop_signal = TRUE; switch (event->keyval) { case GDK_Return: case GDK_KP_Enter: dupe_menu_view(dw, di, listview, FALSE); break; case 'V': case 'v': dupe_menu_view(dw, di, listview, TRUE); break; case GDK_Delete: case GDK_KP_Delete: dupe_window_remove_selection(dw, listview); break; case 'C': case 'c': if (!on_second) { dupe_window_collection_from_selection(dw); } break; case '1': dupe_listview_select_dupes(dw, TRUE); break; case '2': dupe_listview_select_dupes(dw, FALSE); break; case GDK_Menu: case GDK_F10: if (!on_second) { GtkWidget *menu; menu = dupe_menu_popup_main(dw, di); gtk_menu_popup(GTK_MENU(menu), NULL, NULL, dupe_popup_menu_pos_cb, listview, 0, GDK_CURRENT_TIME); } else { GtkWidget *menu; menu = dupe_menu_popup_second(dw, di); gtk_menu_popup(GTK_MENU(menu), NULL, NULL, dupe_popup_menu_pos_cb, listview, 0, GDK_CURRENT_TIME); } break; default: stop_signal = FALSE; break; } } return stop_signal; } void dupe_window_clear(DupeWindow *dw) { GtkListStore *store; dupe_check_stop(dw); store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(dw->listview))); gtk_list_store_clear(store); gtk_tree_view_columns_autosize(GTK_TREE_VIEW(dw->listview)); g_list_free(dw->dupes); dw->dupes = NULL; dupe_list_free(dw->list); dw->list = NULL; dupe_match_reset_list(dw->second_list); dupe_window_update_count(dw, FALSE); dupe_window_update_progress(dw, NULL, 0.0, FALSE); } void dupe_window_close(DupeWindow *dw) { dupe_check_stop(dw); dupe_window_list = g_list_remove(dupe_window_list, dw); gtk_widget_destroy(dw->window); g_list_free(dw->dupes); dupe_list_free(dw->list); dupe_list_free(dw->second_list); g_free(dw); } static gint dupe_window_delete(GtkWidget *widget, GdkEvent *event, gpointer data) { DupeWindow *dw = data; dupe_window_close(dw); return TRUE; } /* collection and files can be NULL */ DupeWindow *dupe_window_new(DupeMatchType match_mask) { DupeWindow *dw; GtkWidget *vbox; GtkWidget *scrolled; GtkWidget *frame; GtkWidget *status_box; GtkWidget *label; GtkWidget *button; GtkListStore *store; GtkTreeSelection *selection; GdkGeometry geometry; dw = g_new0(DupeWindow, 1); dw->list = NULL; dw->dupes = NULL; dw->match_mask = match_mask; dw->show_thumbs = FALSE; dw->idle_id = -1; dw->second_set = FALSE; dw->window = window_new(GTK_WINDOW_TOPLEVEL, "dupe", NULL, NULL, _("Find duplicates")); geometry.min_width = 32; geometry.min_height = 32; geometry.base_width = DUPE_DEF_WIDTH; geometry.base_height = DUPE_DEF_HEIGHT; gtk_window_set_geometry_hints(GTK_WINDOW(dw->window), NULL, &geometry, GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE); gtk_window_set_default_size(GTK_WINDOW(dw->window), DUPE_DEF_WIDTH, DUPE_DEF_HEIGHT); gtk_window_set_resizable(GTK_WINDOW(dw->window), TRUE); gtk_container_set_border_width(GTK_CONTAINER(dw->window), 0); g_signal_connect(G_OBJECT(dw->window), "delete_event", G_CALLBACK(dupe_window_delete), dw); g_signal_connect(G_OBJECT(dw->window), "key_press_event", G_CALLBACK(dupe_window_keypress_cb), dw); vbox = gtk_vbox_new(FALSE, 0); gtk_container_add(GTK_CONTAINER(dw->window), vbox); gtk_widget_show(vbox); dw->table = gtk_table_new(1, 3, FALSE); gtk_box_pack_start(GTK_BOX(vbox), dw->table, TRUE, TRUE, 0); gtk_widget_show(dw->table); scrolled = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_table_attach_defaults(GTK_TABLE(dw->table), scrolled, 0, 2, 0, 1); gtk_widget_show(scrolled); store = gtk_list_store_new(9, G_TYPE_POINTER, G_TYPE_STRING, GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN); dw->listview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store)); g_object_unref(store); selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->listview)); gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE); gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(dw->listview), TRUE); gtk_tree_view_set_enable_search(GTK_TREE_VIEW(dw->listview), FALSE); dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_RANK, "", FALSE, TRUE); dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_THUMB, "", TRUE, FALSE); dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_NAME, _("Name"), FALSE, FALSE); dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_SIZE, _("Size"), FALSE, TRUE); dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_DATE, _("Date"), FALSE, TRUE); dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_DIMENSIONS, _("Dimensions"), FALSE, FALSE); dupe_listview_add_column(dw, dw->listview, DUPE_COLUMN_PATH, _("Path"), FALSE, FALSE); gtk_container_add(GTK_CONTAINER(scrolled), dw->listview); gtk_widget_show(dw->listview); dw->second_vbox = gtk_vbox_new(FALSE, 0); gtk_table_attach_defaults(GTK_TABLE(dw->table), dw->second_vbox, 2, 3, 0, 1); if (dw->second_set) { gtk_table_set_col_spacings(GTK_TABLE(dw->table), PREF_PAD_GAP); gtk_widget_show(dw->second_vbox); } else { gtk_table_set_col_spacings(GTK_TABLE(dw->table), 0); } scrolled = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_box_pack_start(GTK_BOX(dw->second_vbox), scrolled, TRUE, TRUE, 0); gtk_widget_show(scrolled); store = gtk_list_store_new(2, G_TYPE_POINTER, G_TYPE_STRING); dw->second_listview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store)); selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(dw->second_listview)); gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_MULTIPLE); gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(dw->second_listview), TRUE); gtk_tree_view_set_enable_search(GTK_TREE_VIEW(dw->second_listview), FALSE); dupe_listview_add_column(dw, dw->second_listview, 1, _("Compare to:"), FALSE, FALSE); gtk_container_add(GTK_CONTAINER(scrolled), dw->second_listview); gtk_widget_show(dw->second_listview); dw->second_status_label = gtk_label_new(""); gtk_box_pack_start(GTK_BOX(dw->second_vbox), dw->second_status_label, FALSE, FALSE, 0); gtk_widget_show(dw->second_status_label); pref_line(dw->second_vbox, GTK_ORIENTATION_HORIZONTAL); status_box = pref_box_new(vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 0); label = gtk_label_new(_("Compare by:")); gtk_box_pack_start(GTK_BOX(status_box), label, FALSE, FALSE, PREF_PAD_SPACE); gtk_widget_show(label); dupe_menu_setup(dw); gtk_box_pack_start(GTK_BOX(status_box), dw->combo, FALSE, FALSE, 0); gtk_widget_show(dw->combo); dw->button_thumbs = gtk_check_button_new_with_label(_("Thumbnails")); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dw->button_thumbs), dw->show_thumbs); g_signal_connect(G_OBJECT(dw->button_thumbs), "toggled", G_CALLBACK(dupe_window_show_thumb_cb), dw); gtk_box_pack_start(GTK_BOX(status_box), dw->button_thumbs, FALSE, FALSE, PREF_PAD_SPACE); gtk_widget_show(dw->button_thumbs); button = gtk_check_button_new_with_label(_("Compare two file sets")); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), dw->second_set); g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(dupe_second_set_toggle_cb), dw); gtk_box_pack_end(GTK_BOX(status_box), button, FALSE, FALSE, PREF_PAD_SPACE); gtk_widget_show(button); status_box = gtk_hbox_new(FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), status_box, FALSE, FALSE, 0); gtk_widget_show(status_box); frame = gtk_frame_new(NULL); gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN); gtk_box_pack_start(GTK_BOX(status_box), frame, TRUE, TRUE, 0); gtk_widget_show(frame); dw->status_label = gtk_label_new(""); gtk_container_add(GTK_CONTAINER(frame), dw->status_label); gtk_widget_show(dw->status_label); dw->extra_label = gtk_progress_bar_new(); gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dw->extra_label), 0.0); gtk_box_pack_end(GTK_BOX(status_box), dw->extra_label, FALSE, FALSE, 0); gtk_widget_show(dw->extra_label); dupe_dnd_init(dw); /* order is important here, dnd_init should be seeing mouse * presses before we possibly handle (and stop) the signal */ g_signal_connect(G_OBJECT(dw->listview), "button_press_event", G_CALLBACK(dupe_listview_press_cb), dw); g_signal_connect(G_OBJECT(dw->listview), "button_release_event", G_CALLBACK(dupe_listview_release_cb), dw); g_signal_connect(G_OBJECT(dw->second_listview), "button_press_event", G_CALLBACK(dupe_listview_press_cb), dw); g_signal_connect(G_OBJECT(dw->second_listview), "button_release_event", G_CALLBACK(dupe_listview_release_cb), dw); gtk_widget_show(dw->window); dupe_window_update_count(dw, TRUE); dupe_window_update_progress(dw, NULL, 0.0, FALSE); dupe_window_list = g_list_append(dupe_window_list, dw); return dw; } /* *------------------------------------------------------------------- * dnd confirm dir *------------------------------------------------------------------- */ typedef struct { DupeWindow *dw; GList *list; } CDupeConfirmD; static void confirm_dir_list_cancel(GtkWidget *widget, gpointer data) { /* do nothing */ } static void confirm_dir_list_add(GtkWidget *widget, gpointer data) { CDupeConfirmD *d = data; GList *work; dupe_window_add_files(d->dw, d->list, FALSE); work = d->list; while (work) { FileData *fd = work->data; work = work->next; if (isdir(fd->path)) { GList *list = NULL; filelist_read(fd->path, &list, NULL); list = filelist_filter(list, FALSE); if (list) { dupe_window_add_files(d->dw, list, FALSE); filelist_free(list); } } } } static void confirm_dir_list_recurse(GtkWidget *widget, gpointer data) { CDupeConfirmD *d = data; dupe_window_add_files(d->dw, d->list, TRUE); } static void confirm_dir_list_skip(GtkWidget *widget, gpointer data) { CDupeConfirmD *d = data; dupe_window_add_files(d->dw, d->list, FALSE); } static void confirm_dir_list_destroy(GtkWidget *widget, gpointer data) { CDupeConfirmD *d = data; filelist_free(d->list); g_free(d); } static GtkWidget *dupe_confirm_dir_list(DupeWindow *dw, GList *list) { GtkWidget *menu; CDupeConfirmD *d; d = g_new0(CDupeConfirmD, 1); d->dw = dw; d->list = list; menu = popup_menu_short_lived(); g_signal_connect(G_OBJECT(menu), "destroy", G_CALLBACK(confirm_dir_list_destroy), d); menu_item_add_stock(menu, _("Dropped list includes folders."), GTK_STOCK_DND_MULTIPLE, NULL, NULL); menu_item_add_divider(menu); menu_item_add_stock(menu, _("_Add contents"), GTK_STOCK_OK, G_CALLBACK(confirm_dir_list_add), d); menu_item_add_stock(menu, _("Add contents _recursive"), GTK_STOCK_ADD, G_CALLBACK(confirm_dir_list_recurse), d); menu_item_add_stock(menu, _("_Skip folders"), GTK_STOCK_REMOVE, G_CALLBACK(confirm_dir_list_skip), d); menu_item_add_divider(menu); menu_item_add_stock(menu, _("Cancel"), GTK_STOCK_CANCEL, G_CALLBACK(confirm_dir_list_cancel), d); return menu; } /* *------------------------------------------------------------------- * dnd *------------------------------------------------------------------- */ static GtkTargetEntry dupe_drag_types[] = { { "text/uri-list", 0, TARGET_URI_LIST }, { "text/plain", 0, TARGET_TEXT_PLAIN } }; static gint n_dupe_drag_types = 2; static GtkTargetEntry dupe_drop_types[] = { { TARGET_APP_COLLECTION_MEMBER_STRING, 0, TARGET_APP_COLLECTION_MEMBER }, { "text/uri-list", 0, TARGET_URI_LIST } }; static gint n_dupe_drop_types = 2; static void dupe_dnd_data_set(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection_data, guint info, guint time, gpointer data) { DupeWindow *dw = data; gchar *uri_text; gint length; GList *list; switch (info) { case TARGET_URI_LIST: case TARGET_TEXT_PLAIN: list = dupe_listview_get_selection(dw, widget); if (!list) return; uri_text = uri_text_from_filelist(list, &length, (info == TARGET_TEXT_PLAIN)); filelist_free(list); break; default: uri_text = NULL; break; } if (uri_text) gtk_selection_data_set(selection_data, selection_data->target, 8, (guchar *)uri_text, length); g_free(uri_text); } static void dupe_dnd_data_get(GtkWidget *widget, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection_data, guint info, guint time, gpointer data) { DupeWindow *dw = data; GtkWidget *source; GList *list = NULL; GList *work; source = gtk_drag_get_source_widget(context); if (source == dw->listview || source == dw->second_listview) return; dw->second_drop = (dw->second_set && widget == dw->second_listview); switch (info) { case TARGET_APP_COLLECTION_MEMBER: collection_from_dnd_data((gchar *)selection_data->data, &list, NULL); break; case TARGET_URI_LIST: list = uri_filelist_from_text((gchar *)selection_data->data, TRUE); work = list; while (work) { FileData *fd = work->data; if (isdir(fd->path)) { GtkWidget *menu; menu = dupe_confirm_dir_list(dw, list); gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, 0, time); return; } work = work->next; } break; default: list = NULL; break; } if (list) { dupe_window_add_files(dw, list, FALSE); filelist_free(list); } } static void dupe_dest_set(GtkWidget *widget, gint enable) { if (enable) { gtk_drag_dest_set(widget, GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_HIGHLIGHT | GTK_DEST_DEFAULT_DROP, dupe_drop_types, n_dupe_drop_types, GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_ASK); } else { gtk_drag_dest_unset(widget); } } static void dupe_dnd_begin(GtkWidget *widget, GdkDragContext *context, gpointer data) { DupeWindow *dw = data; dupe_dest_set(dw->listview, FALSE); dupe_dest_set(dw->second_listview, FALSE); if (dw->click_item && !dupe_listview_item_is_selected(dw, dw->click_item, widget)) { GtkListStore *store; GtkTreeIter iter; store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(widget))); if (dupe_listview_find_item(store, dw->click_item, &iter) >= 0) { GtkTreeSelection *selection; GtkTreePath *tpath; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget)); gtk_tree_selection_unselect_all(selection); gtk_tree_selection_select_iter(selection, &iter); tpath = gtk_tree_model_get_path(GTK_TREE_MODEL(store), &iter); gtk_tree_view_set_cursor(GTK_TREE_VIEW(widget), tpath, NULL, FALSE); gtk_tree_path_free(tpath); } } if (dw->show_thumbs && widget == dw->listview && dw->click_item && dw->click_item->pixbuf) { GtkTreeSelection *selection; gint items; selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget)); items = gtk_tree_selection_count_selected_rows(selection); dnd_set_drag_icon(widget, context, dw->click_item->pixbuf, items); } } static void dupe_dnd_end(GtkWidget *widget, GdkDragContext *context, gpointer data) { DupeWindow *dw = data; dupe_dest_set(dw->listview, TRUE); dupe_dest_set(dw->second_listview, TRUE); } static void dupe_dnd_init(DupeWindow *dw) { gtk_drag_source_set(dw->listview, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK, dupe_drag_types, n_dupe_drag_types, GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK); g_signal_connect(G_OBJECT(dw->listview), "drag_data_get", G_CALLBACK(dupe_dnd_data_set), dw); g_signal_connect(G_OBJECT(dw->listview), "drag_begin", G_CALLBACK(dupe_dnd_begin), dw); g_signal_connect(G_OBJECT(dw->listview), "drag_end", G_CALLBACK(dupe_dnd_end), dw); dupe_dest_set(dw->listview, TRUE); g_signal_connect(G_OBJECT(dw->listview), "drag_data_received", G_CALLBACK(dupe_dnd_data_get), dw); gtk_drag_source_set(dw->second_listview, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK, dupe_drag_types, n_dupe_drag_types, GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK); g_signal_connect(G_OBJECT(dw->second_listview), "drag_data_get", G_CALLBACK(dupe_dnd_data_set), dw); g_signal_connect(G_OBJECT(dw->second_listview), "drag_begin", G_CALLBACK(dupe_dnd_begin), dw); g_signal_connect(G_OBJECT(dw->second_listview), "drag_end", G_CALLBACK(dupe_dnd_end), dw); dupe_dest_set(dw->second_listview, TRUE); g_signal_connect(G_OBJECT(dw->second_listview), "drag_data_received", G_CALLBACK(dupe_dnd_data_get), dw); } /* *------------------------------------------------------------------- * maintenance (move, delete, etc.) *------------------------------------------------------------------- */ void dupe_maint_removed(FileData *fd) { GList *work; work = dupe_window_list; while (work) { DupeWindow *dw = work->data; work = work->next; while (dupe_item_remove_by_path(dw, fd->path)); } } void dupe_maint_renamed(FileData *fd) { GList *work; work = dupe_window_list; while (work) { DupeWindow *dw = work->data; work = work->next; dupe_item_update_fd(dw, fd); } }