1 /*
2  * This program is free software; you can redistribute it and/or
3  * modify it under the terms of the GNU General Public License
4  * as published by the Free Software Foundation; either version 2
5  * of the License, or (at your option) any later version.
6  *
7  * This program is distributed in the hope that it will be useful,
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  * GNU General Public License for more details.
11  *
12  * You should have received a copy of the GNU General Public License
13  * along with this program; if not, write to the Free Software
14  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  *
16  * See the COPYING file for license information.
17  *
18  * Guillaume Chazarain <guichaz@gmail.com>
19  */
20 
21 /******************
22  * The files list *
23  ******************/
24 
25 #include <sys/stat.h>           /* stat() */
26 #include <stdlib.h>             /* qsort() */
27 #include <stdio.h>              /* remove(), perror(), stdin, getdelim() */
28 #include <sys/types.h>          /* size_t */
29 
30 #include "gliv.h"
31 #include "files_list.h"
32 #include "options.h"
33 #include "loading.h"
34 #include "str_utils.h"
35 #include "foreach_file.h"
36 #include "next_image.h"
37 #include "messages.h"
38 #include "windows.h"
39 #include "collection.h"
40 #include "formats.h"
41 #include "timestamp.h"
42 #include "pathset.h"
43 #include "strnatcmp.h"
44 
45 #ifndef HAVE_GETDELIM
46 #include "../lib/getdelim.h"
47 #endif
48 
49 extern options_struct *options;
50 extern GlivImage *current_image;
51 
52 static GList *files_list = NULL;
53 static gint list_length = 0;
54 static GList *list_end = NULL;
55 static DECLARE_TIMESTAMP(timestamp);    /* Last modification. */
56 static GList *obsolete_nodes = NULL;    /* Nodes to delete when possible */
57 
get_list_length(void)58 gint get_list_length(void)
59 {
60     return list_length;
61 }
62 
get_list_head(void)63 GList *get_list_head(void)
64 {
65     return files_list;
66 }
67 
get_list_end(void)68 GList *get_list_end(void)
69 {
70     return list_end;
71 }
72 
73 /*** Additions. ***/
74 
is_loadable(const gchar * filename)75 static gboolean is_loadable(const gchar * filename)
76 {
77     loader_t loader;
78 
79     if (options->force)
80         return TRUE;
81 
82     loader = get_loader(filename);
83     return loader == LOADER_PIXBUF || loader == LOADER_DECOMP_PIXBUF;
84 }
85 
86 /* Returns the number of files added. */
add_file_to_list(const gchar * filename)87 static gint add_file_to_list(const gchar * filename)
88 {
89     if (is_loadable(filename)) {
90         list_end = g_list_append(list_end, clean_filename(filename));
91         list_length++;
92 
93         if (list_length == 1)
94             files_list = list_end;
95         else
96             list_end = list_end->next;
97 
98         touch(&timestamp);
99         return 1;
100     }
101 
102     return 0;
103 }
104 
105 struct files_list_state {
106     GList *files_list;
107     gint list_length;
108     GList *list_end;
109 };
110 
save_files_list_state(void)111 static struct files_list_state *save_files_list_state(void)
112 {
113     struct files_list_state *prev = g_new(struct files_list_state, 1);
114 
115     prev->files_list = files_list;
116     prev->list_length = list_length;
117     prev->list_end = list_end;
118 
119     files_list = NULL;
120     list_length = 0;
121     list_end = NULL;
122 
123     return prev;
124 }
125 
merge_files_list_state(struct files_list_state * prev,gboolean after_current)126 static void merge_files_list_state(struct files_list_state *prev,
127                                    gboolean after_current)
128 {
129     if (prev->list_length == 0) {
130         /* The previous list was empty => nothing to merge. */
131         goto end_free;
132     }
133 
134     if (files_list == NULL) {
135         /* No files were added. */
136         files_list = prev->files_list;
137         list_length = prev->list_length;
138         list_end = prev->list_end;
139         goto end_free;
140     }
141 
142     if (after_current && current_image != NULL &&
143         current_image->node->next != NULL) {
144         /* Insert after the current image. */
145 
146         /* Merge the insertion end. */
147         list_end->next = current_image->node->next;
148         list_end->next->prev = list_end;
149 
150         /* Merge the insertion beginning. */
151         current_image->node->next = files_list;
152         files_list->prev = current_image->node;
153 
154         files_list = prev->files_list;
155     } else {
156         /* Insert at the end. */
157         if (prev->list_end != NULL)
158             prev->list_end->next = files_list;
159 
160         files_list->prev = prev->list_end;
161         files_list = prev->files_list;
162     }
163 
164     list_length += prev->list_length;
165 
166   end_free:
167 
168     g_free(prev);
169 }
170 
171 static void reorder_list(gboolean shuffle);
172 
cmp_filename(gconstpointer a,gconstpointer b)173 static gint cmp_filename(gconstpointer a, gconstpointer b)
174 {
175     return !g_str_equal(a, b);
176 }
177 
178 /* Returns the number of files added. */
add_dir(const gchar * dirname,const gchar * first_file)179 static gint add_dir(const gchar * dirname, const gchar * first_file)
180 {
181     gint nb_inserted = 0;
182     struct files_list_state *prev = save_files_list_state();
183     GList *place_first = NULL;
184 
185     if (options->recursive) {
186         /* Traverse recursively the directory. */
187         nb_inserted += foreach_file(dirname, add_file_to_list);
188     } else {
189         /* Add every file in the directory. */
190         GDir *dir;
191         GError *err = NULL;
192         const gchar *dir_entry;
193 
194         dir = g_dir_open(dirname, 0, &err);
195         if (dir == NULL) {
196             g_printerr("%s\n", err->message);
197             g_error_free(err);
198             return 0;
199         }
200 
201         while ((dir_entry = g_dir_read_name(dir)) != NULL) {
202             gchar *full_path = g_build_filename(dirname, dir_entry, NULL);
203 
204             if (!g_file_test(full_path, G_FILE_TEST_IS_DIR))
205                 /* We have a file. */
206                 nb_inserted += add_file_to_list(full_path);
207 
208             g_free(full_path);
209         }
210 
211         g_dir_close(dir);
212     }
213 
214     reorder_list(FALSE);
215 
216     if (first_file != NULL)
217         place_first = g_list_find_custom(files_list, first_file, cmp_filename);
218 
219     if (place_first != NULL && place_first->prev != NULL) {
220         place_first->prev->next = place_first->next;
221 
222         if (place_first->next != NULL)
223             place_first->next->prev = place_first->prev;
224 
225         place_first->prev = NULL;
226         place_first->next = files_list;
227 
228         files_list->prev = place_first;
229 
230         files_list = place_first;
231     }
232 
233     merge_files_list_state(prev, FALSE);
234     return nb_inserted;
235 }
236 
237 /* Returns the number of files added. */
add_to_list(const gchar * name,gboolean add_all)238 static gint add_to_list(const gchar * name, gboolean add_all)
239 {
240     gint nb_inserted = 0;
241     struct stat st;
242 
243     if (stat(name, &st) < 0) {
244         perror(name);
245         return 0;
246     }
247 
248     if (S_ISDIR(st.st_mode)) {
249         nb_inserted += add_dir(name, NULL);
250     } else {
251         loader_t loader = get_loader(name);
252 
253         if (loader == LOADER_DOT_GLIV || loader == LOADER_DECOMP_DOT_GLIV)
254             /* A .gliv collection. */
255             nb_inserted += load_dot_gliv(name);
256 
257         else if (add_all) {
258             gchar *dirname = g_path_get_dirname(name);
259             gchar *clean = clean_filename(name);
260 
261             nb_inserted += add_dir(dirname, clean);
262             g_free(dirname);
263             g_free(clean);
264         } else {
265             nb_inserted += add_file_to_list(name);
266         }
267     }
268 
269     return nb_inserted;
270 }
271 
272 /*** Deletion ***/
273 
remove_from_list(GList * node)274 void remove_from_list(GList * node)
275 {
276     if (node == list_end)
277         list_end = node->prev;
278 
279     if (current_image && current_image->node == node)
280         current_image->node = NULL;
281 
282     unload(node);
283     g_free(node->data);
284     files_list = g_list_delete_link(files_list, node);
285     list_length--;
286     touch(&timestamp);
287 }
288 
289 /*** Sorting ***/
290 
291 /* To shuffle the list, we simply sort with a random compare func. */
random_compar(gconstpointer unused1,gconstpointer unused2)292 static gint random_compar(gconstpointer unused1, gconstpointer unused2)
293 {
294     return g_random_int_range(-1, 3);
295 }
296 
297 /* We want children to be after their parent, or the alphabetical order. */
filename_compar(gconstpointer a,gconstpointer b)298 G_GNUC_PURE static gint filename_compar(gconstpointer a, gconstpointer b)
299 {
300     const gchar *ptr1, *ptr2;
301     gboolean ptr1_has_dir = FALSE, ptr2_has_dir = FALSE;
302     gint prefix_length;
303 
304     ptr1 = (const gchar *) a;
305     ptr2 = (const gchar *) b;
306 
307     if (ptr1[0] != ptr2[0])
308         /* Comparing an absolute filename, and a relative one. */
309         return ptr1[0] == '/' ? -1 : 1;
310 
311     prefix_length = common_prefix_length(ptr1, ptr2);
312     ptr1 += prefix_length;
313     ptr2 += prefix_length;
314 
315     if (*ptr1 == *ptr2)
316         /* The filenames were equal. */
317         return 0;
318 
319     /* Go back to the first different dir. */
320     for (;;) {
321         ptr1--;
322         ptr2--;
323 
324         if (*ptr1 == '/') {
325             if (*ptr2 == '/')
326                 break;
327 
328             ptr1_has_dir = TRUE;
329         } else if (*ptr2 == '/') {
330             ptr2_has_dir = TRUE;
331         }
332     }
333 
334     /* Skip the common '/'. */
335     ptr1++;
336     ptr2++;
337 
338     if (ptr1_has_dir == ptr2_has_dir)
339         /*
340          * Either the files are in the same directory,
341          * or they are not parent.
342          */
343         return strnatcmp(ptr1, ptr2);
344 
345     /* One of the directory is parent of the other one. */
346     return ptr1_has_dir ? -1 : 1;
347 }
348 
reorder_list(gboolean shuffle)349 static void reorder_list(gboolean shuffle)
350 {
351     GCompareFunc compare_func;
352 
353     compare_func = shuffle ? random_compar : filename_compar;
354     files_list = g_list_sort(files_list, compare_func);
355     list_end = g_list_last(list_end);
356 }
357 
358 /* Called by the menu. */
reorder_files(gboolean shuffle)359 gboolean reorder_files(gboolean shuffle)
360 {
361     if (files_list == NULL)
362         return FALSE;
363 
364     reorder_list(shuffle);
365 
366     after_reorder();
367     touch(&timestamp);
368     return FALSE;
369 }
370 
compar(gconstpointer a,gconstpointer b)371 G_GNUC_PURE static gint compar(gconstpointer a, gconstpointer b)
372 {
373     return filename_compar(*((const gchar **) a), *((const gchar **) b));
374 }
375 
376 /*
377  * Used to build the images menus.
378  */
get_sorted_files_array(void)379 gchar **get_sorted_files_array(void)
380 {
381     gchar **array, **array_ptr;
382     GList *list_ptr;
383 
384     if (list_length == 0)
385         return NULL;
386 
387     array_ptr = array = g_new(gchar *, list_length + 1);
388 
389     /* Fill the array. */
390     for (list_ptr = files_list; list_ptr != NULL; list_ptr = list_ptr->next) {
391         *array_ptr = list_ptr->data;
392         array_ptr++;
393     }
394 
395     *array_ptr = NULL;
396     qsort(array, list_length, sizeof(gchar *), compar);
397 
398     return array;
399 }
400 
401 /*** Initialization ***/
402 
list_initialized(gboolean sort,gboolean shuffle)403 static void list_initialized(gboolean sort, gboolean shuffle)
404 {
405     if (sort || shuffle)
406         reorder_list(shuffle);
407 
408     touch(&timestamp);
409 }
410 
init_from_null_filenames(gboolean sort,gboolean shuffle,gboolean add_all)411 gint init_from_null_filenames(gboolean sort, gboolean shuffle, gboolean add_all)
412 {
413     gchar *filename = NULL;
414     size_t len = 0;
415     gint nb_inserted = 0;
416 
417     while (!feof(stdin) && getdelim(&filename, &len, '\0', stdin) > 0)
418         nb_inserted += add_to_list(filename, add_all);
419 
420     list_initialized(sort, shuffle);
421     g_free(filename);
422 
423     return nb_inserted;
424 }
425 
init_list(gchar ** names,gint nb,gboolean sort,gboolean shuffle,gboolean add_all)426 gint init_list(gchar ** names, gint nb, gboolean sort, gboolean shuffle,
427                gboolean add_all)
428 {
429     gint nb_inserted;
430 
431     nb_inserted = insert_after_current(names, nb, FALSE, add_all);
432     list_initialized(sort, shuffle);
433 
434     return nb_inserted;
435 }
436 
437 /*** Misc. operations. ***/
438 
439 /* Returns the number of files inserted. */
insert_after_current(gchar ** names,gint nb,gboolean just_file,gboolean add_all)440 gint insert_after_current(gchar ** names, gint nb, gboolean just_file,
441                           gboolean add_all)
442 {
443     gint nb_inserted = 0;
444     struct files_list_state *prev = save_files_list_state();
445     struct pathset *paths = pathset_new();
446 
447     for (; nb != 0; names++, nb--) {
448         if (just_file)
449             nb_inserted += add_file_to_list(*names);
450 
451         else if (!pathset_add(paths, *names))
452             /* Already added */
453             continue;
454 
455         else if (add_all && !g_file_test(*names, G_FILE_TEST_IS_DIR)) {
456             gchar *dirname = g_path_get_dirname(*names);
457 
458             if (pathset_add(paths, dirname))
459                 nb_inserted += add_to_list(*names, TRUE);
460 
461             g_free(dirname);
462 
463         } else
464             nb_inserted += add_to_list(*names, FALSE);
465     }
466 
467     pathset_free(paths);
468     list_initialized(FALSE, FALSE);
469     merge_files_list_state(prev, TRUE);
470     return nb_inserted;
471 }
472 
show_remove_dialog(const gchar * msg)473 static gboolean show_remove_dialog(const gchar * msg)
474 {
475     GtkMessageDialog *dialog;
476     gint res;
477 
478     dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new(NULL,
479                                                        GTK_DIALOG_MODAL,
480                                                        GTK_MESSAGE_QUESTION,
481                                                        GTK_BUTTONS_OK_CANCEL,
482                                                        "%s", msg));
483 
484     res = run_modal_dialog(GTK_DIALOG(dialog));
485     gtk_widget_destroy(GTK_WIDGET(dialog));
486 
487     return res == GTK_RESPONSE_ACCEPT || res == GTK_RESPONSE_OK ||
488         res == GTK_RESPONSE_YES;
489 }
490 
confirm_remove_current(void)491 gboolean confirm_remove_current(void)
492 {
493     gchar *filename, *msg;
494 
495     if (current_image == NULL || current_image->node == NULL)
496         return FALSE;
497 
498     filename = current_image->node->data;
499     msg = g_strdup_printf(_("Do you really want to delete this file?\n%s\n"),
500                           filename_to_utf8(filename));
501 
502     if (show_remove_dialog(msg)) {
503         add_obsolete_node(current_image->node);
504 
505         if (remove(filename) < 0)
506             perror(filename);
507     }
508 
509     g_free(msg);
510     return FALSE;
511 }
512 
get_list_timestamp(void)513 timestamp_t get_list_timestamp(void)
514 {
515     return timestamp;
516 }
517 
get_nth_filename(gint n)518 const gchar *get_nth_filename(gint n)
519 {
520     GList *ptr;
521 
522     if (n < 0)
523         return _("directory/file");
524 
525     if (n <= get_list_length() / 2)
526         for (ptr = get_list_head(); n != 0; n--, ptr = ptr->next);
527     else {
528         n = get_list_length() - n - 1;
529         for (ptr = get_list_end(); n != 0; n--, ptr = ptr->prev);
530     }
531 
532     return ptr->data;
533 }
534 
get_image_number(GlivImage * im)535 gint get_image_number(GlivImage * im)
536 {
537     if (im->number < 0) {
538         GList *ptr;
539         gint im_nr = 0;
540 
541         if (im->node == NULL)
542             return im->number;
543 
544         for (ptr = get_list_head(); ptr != NULL; ptr = ptr->next) {
545             if (ptr == im->node) {
546                 im->number = im_nr;
547                 break;
548             }
549 
550             im_nr++;
551         }
552     }
553 
554     return im->number;
555 }
556 
find_node_by_name(const gchar * name)557 GList *find_node_by_name(const gchar * name)
558 {
559     GList *ptr;
560 
561     for (ptr = get_list_head(); ptr != NULL; ptr = ptr->next)
562         if (ptr->data == name)
563             return ptr;
564 
565     return NULL;
566 }
567 
add_obsolete_node(GList * node)568 void add_obsolete_node(GList * node)
569 {
570     obsolete_nodes = g_list_prepend(obsolete_nodes, node);
571 }
572 
remove_obsolete_nodes(void)573 gboolean remove_obsolete_nodes(void)
574 {
575     GList *ptr, *next;
576     gboolean destroyed = FALSE;
577 
578     for (ptr = obsolete_nodes; ptr != NULL; ptr = next) {
579         next = ptr->next;
580         if (ptr->data != current_image->node) {
581             remove_from_list(ptr->data);
582             obsolete_nodes = g_list_delete_link(obsolete_nodes, ptr);
583             destroyed = TRUE;
584         }
585     }
586 
587     if (destroyed)
588         touch(&timestamp);
589 
590     return destroyed;
591 }
592