098473529d317fae32514d7378b496ed60f2ef58
[geeqie.git] / src / collect.cc
1 /*
2  * Copyright (C) 2006 John Ellis
3  * Copyright (C) 2008 - 2016 The Geeqie Team
4  *
5  * Author: John Ellis
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 #include "main.h"
23 #include "collect.h"
24
25 #include "collect-dlg.h"
26 #include "collect-io.h"
27 #include "collect-table.h"
28 #include "filedata.h"
29 #include "img-view.h"
30 #include "layout-image.h"
31 #include "layout-util.h"
32 #include "misc.h"
33 #include "pixbuf-util.h"
34 #include "print.h"
35 #include "ui-fileops.h"
36 #include "ui-tree-edit.h"
37 #include "utilops.h"
38 #include "window.h"
39
40 #define COLLECT_DEF_WIDTH 440
41 #define COLLECT_DEF_HEIGHT 450
42
43 /**
44  *  list of paths to collections */
45
46 /**
47  * @brief  List of currently open Collections.
48  * 
49  * Type ::_CollectionData 
50  */
51 static GList *collection_list = NULL;
52
53 /**
54  * @brief  List of currently open Collection windows.
55  * 
56  * Type ::_CollectWindow
57  */
58 static GList *collection_window_list = NULL;
59
60 static void collection_window_get_geometry(CollectWindow *cw);
61 static void collection_window_refresh(CollectWindow *cw);
62 static void collection_window_update_title(CollectWindow *cw);
63 static void collection_window_add(CollectWindow *cw, CollectInfo *ci);
64 static void collection_window_insert(CollectWindow *cw, CollectInfo *ci);
65 static void collection_window_remove(CollectWindow *cw, CollectInfo *ci);
66 static void collection_window_update(CollectWindow *cw, CollectInfo *ci);
67
68 static void collection_window_close(CollectWindow *cw);
69
70 static void collection_notify_cb(FileData *fd, NotifyType type, gpointer data);
71
72 /*
73  *-------------------------------------------------------------------
74  * data, list handling
75  *-------------------------------------------------------------------
76  */
77
78 CollectInfo *collection_info_new(FileData *fd, struct stat *UNUSED(st), GdkPixbuf *pixbuf)
79 {
80         CollectInfo *ci;
81
82         if (!fd) return NULL;
83
84         ci = g_new0(CollectInfo, 1);
85         ci->fd = file_data_ref(fd);
86
87         ci->pixbuf = pixbuf;
88         if (ci->pixbuf) g_object_ref(ci->pixbuf);
89
90         return ci;
91 }
92
93 void collection_info_free_thumb(CollectInfo *ci)
94 {
95         if (ci->pixbuf) g_object_unref(ci->pixbuf);
96         ci->pixbuf = NULL;
97 }
98
99 void collection_info_free(CollectInfo *ci)
100 {
101         if (!ci) return;
102
103         file_data_unref(ci->fd);
104         collection_info_free_thumb(ci);
105         g_free(ci);
106 }
107
108 void collection_info_set_thumb(CollectInfo *ci, GdkPixbuf *pixbuf)
109 {
110         if (pixbuf) g_object_ref(pixbuf);
111         collection_info_free_thumb(ci);
112         ci->pixbuf = pixbuf;
113 }
114
115 //gboolean collection_info_load_thumb(CollectInfo *ci)
116 //{
117         //if (!ci) return FALSE;
118
119         //collection_info_free_thumb(ci);
120
121         //log_printf("collection_info_load_thumb not implemented!\n(because an instant thumb loader not implemented)");
122         //return FALSE;
123 //}
124
125 void collection_list_free(GList *list)
126 {
127         GList *work;
128         work = list;
129         while (work)
130                 {
131                 collection_info_free((CollectInfo *)work->data);
132                 work = work->next;
133                 }
134         g_list_free(list);
135 }
136
137 /* an ugly static var, well what ya gonna do ? */
138 static SortType collection_list_sort_method = SORT_NAME;
139
140 static gint collection_list_sort_cb(gconstpointer a, gconstpointer b)
141 {
142         const CollectInfo *cia = a;
143         const CollectInfo *cib = b;
144
145         switch (collection_list_sort_method)
146                 {
147                 case SORT_NAME:
148                         break;
149                 case SORT_NONE:
150                         return 0;
151                         break;
152                 case SORT_SIZE:
153                         if (cia->fd->size < cib->fd->size) return -1;
154                         if (cia->fd->size > cib->fd->size) return 1;
155                         return 0;
156                         break;
157                 case SORT_TIME:
158                         if (cia->fd->date < cib->fd->date) return -1;
159                         if (cia->fd->date > cib->fd->date) return 1;
160                         return 0;
161                         break;
162                 case SORT_CTIME:
163                         if (cia->fd->cdate < cib->fd->cdate) return -1;
164                         if (cia->fd->cdate > cib->fd->cdate) return 1;
165                         return 0;
166                         break;
167                 case SORT_EXIFTIME:
168                         if (cia->fd->exifdate < cib->fd->exifdate) return -1;
169                         if (cia->fd->exifdate > cib->fd->exifdate) return 1;
170                         break;
171                 case SORT_EXIFTIMEDIGITIZED:
172                         if (cia->fd->exifdate_digitized < cib->fd->exifdate_digitized) return -1;
173                         if (cia->fd->exifdate_digitized > cib->fd->exifdate_digitized) return 1;
174                         break;
175                 case SORT_RATING:
176                         if (cia->fd->rating < cib->fd->rating) return -1;
177                         if (cia->fd->rating > cib->fd->rating) return 1;
178                         break;
179                 case SORT_PATH:
180                         return utf8_compare(cia->fd->path, cib->fd->path, options->file_sort.case_sensitive);
181                         break;
182                 case SORT_CLASS:
183                         if (cia->fd->format_class < cib->fd->format_class) return -1;
184                         if (cia->fd->format_class > cib->fd->format_class) return 1;
185                         break;
186 #ifdef HAVE_STRVERSCMP
187                 case SORT_NUMBER:
188                         return strverscmp(cia->fd->name, cib->fd->name);
189                         break;
190 #endif
191                 default:
192                         break;
193                 }
194
195         if (options->file_sort.case_sensitive)
196                 return strcmp(cia->fd->collate_key_name, cib->fd->collate_key_name);
197         else
198                 return strcmp(cia->fd->collate_key_name_nocase, cib->fd->collate_key_name_nocase);
199 }
200
201 GList *collection_list_sort(GList *list, SortType method)
202 {
203         if (method == SORT_NONE) return list;
204
205         collection_list_sort_method = method;
206
207         return g_list_sort(list, collection_list_sort_cb);
208 }
209
210 GList *collection_list_randomize(GList *list)
211 {
212         guint random, length, i;
213         gpointer tmp;
214         GList *nlist, *olist;
215
216         length = g_list_length(list);
217         if (!length) return NULL;
218
219         srand((unsigned int)time(NULL)); // Initialize random generator (hasn't to be that much strong)
220
221         for (i = 0; i < length; i++)
222                 {
223                 random = (guint) (1.0 * length * rand()/(RAND_MAX + 1.0));
224                 olist = g_list_nth(list, i);
225                 nlist = g_list_nth(list, random);
226                 tmp = olist->data;
227                 olist->data = nlist->data;
228                 nlist->data = tmp;
229                 }
230
231         return list;
232 }
233
234 GList *collection_list_add(GList *list, CollectInfo *ci, SortType method)
235 {
236         if (method != SORT_NONE)
237                 {
238                 collection_list_sort_method = method;
239                 list = g_list_insert_sorted(list, ci, collection_list_sort_cb);
240                 }
241         else
242                 {
243                 list = g_list_append(list, ci);
244                 }
245
246         return list;
247 }
248
249 GList *collection_list_insert(GList *list, CollectInfo *ci, CollectInfo *insert_ci, SortType method)
250 {
251         if (method != SORT_NONE)
252                 {
253                 collection_list_sort_method = method;
254                 list = g_list_insert_sorted(list, ci, collection_list_sort_cb);
255                 }
256         else
257                 {
258                 GList *point;
259
260                 point = g_list_find(list, insert_ci);
261                 list = uig_list_insert_link(list, point, ci);
262                 }
263
264         return list;
265 }
266
267 GList *collection_list_remove(GList *list, CollectInfo *ci)
268 {
269         list = g_list_remove(list, ci);
270         collection_info_free(ci);
271         return list;
272 }
273
274 CollectInfo *collection_list_find_fd(GList *list, FileData *fd)
275 {
276         GList *work = list;
277
278         while (work)
279                 {
280                 CollectInfo *ci = work->data;
281                 if (ci->fd == fd) return ci;
282                 work = work->next;
283                 }
284
285         return NULL;
286 }
287
288 GList *collection_list_to_filelist(GList *list)
289 {
290         GList *filelist = NULL;
291         GList *work = list;
292
293         while (work)
294                 {
295                 CollectInfo *info = work->data;
296                 filelist = g_list_prepend(filelist, file_data_ref(info->fd));
297                 work = work->next;
298                 }
299
300         filelist = g_list_reverse(filelist);
301         return filelist;
302 }
303
304 CollectWindow *collection_window_find(CollectionData *cd)
305 {
306         GList *work;
307
308         work = collection_window_list;
309         while (work)
310                 {
311                 CollectWindow *cw = work->data;
312                 if (cw->cd == cd) return cw;
313                 work = work->next;
314                 }
315
316         return NULL;
317 }
318
319 CollectWindow *collection_window_find_by_path(const gchar *path)
320 {
321         GList *work;
322
323         if (!path) return NULL;
324
325         work = collection_window_list;
326         while (work)
327                 {
328                 CollectWindow *cw = work->data;
329                 if (cw->cd->path && strcmp(cw->cd->path, path) == 0) return cw;
330                 work = work->next;
331                 }
332
333         return NULL;
334 }
335
336 /**
337  * @brief Checks string for existence of Collection.
338  * @param[in] param Filename, with or without extension of any collection
339  * @returns full pathname if found or NULL
340  * 
341  * Return value must be freed with g_free()
342  */
343 gchar *collection_path(const gchar *param)
344 {
345         gchar *path = NULL;
346         gchar *full_name = NULL;
347
348         if (file_extension_match(param, GQ_COLLECTION_EXT))
349                 {
350                 path = g_build_filename(get_collections_dir(), param, NULL);
351                 }
352         else if (file_extension_match(param, NULL))
353                 {
354                 full_name = g_strconcat(param, GQ_COLLECTION_EXT, NULL);
355                 path = g_build_filename(get_collections_dir(), full_name, NULL);
356                 }
357
358         if (!isfile(path))
359                 {
360                 g_free(path);
361                 path = NULL;
362                 }
363
364         g_free(full_name);
365         return path;
366 }
367
368 /**
369  * @brief Checks input string for existence of Collection.
370  * @param[in] param Filename with or without extension of any collection
371  * @returns TRUE if found
372  * 
373  * 
374  */
375 gboolean is_collection(const gchar *param)
376 {
377         gchar *name = NULL;
378
379         name = collection_path(param);
380         if (name)
381                 {
382                 g_free(name);
383                 return TRUE;
384                 }
385         return FALSE;
386 }
387
388 /**
389  * @brief Creates a text list of the image paths of the contents of a Collection
390  * @param[in] name The name of the collection, with or without extension
391  * @param[inout] contents A GString to which the image paths are appended
392  * 
393  * 
394  */
395 void collection_contents(const gchar *name, GString **contents)
396 {
397         gchar *path;
398         CollectionData *cd;
399         CollectInfo *ci;
400         GList *work;
401         FileData *fd;
402
403         if (is_collection(name))
404                 {
405                 path = collection_path(name);
406                 cd = collection_new("");
407                 collection_load(cd, path, COLLECTION_LOAD_APPEND);
408                 work = cd->list;
409                 while (work)
410                         {
411                         ci = work->data;
412                         fd = ci->fd;
413                         *contents = g_string_append(*contents, g_strdup(fd->path));
414                         *contents = g_string_append(*contents, "\n");
415
416                         work = work->next;
417                         }
418                 g_free(path);
419                 collection_free(cd);
420                 }
421 }
422
423 /**
424  * @brief Returns a list of filedatas of the contents of a Collection
425  * @param[in] name The name of the collection, with or without extension
426  * 
427  * 
428  */
429 GList *collection_contents_fd(const gchar *name)
430 {
431         gchar *path;
432         CollectionData *cd;
433         CollectInfo *ci;
434         GList *work;
435         GList *list = NULL;
436
437         if (is_collection(name))
438                 {
439                 path = collection_path(name);
440                 cd = collection_new("");
441                 collection_load(cd, path, COLLECTION_LOAD_APPEND);
442                 work = cd->list;
443                 while (work)
444                         {
445                         ci = work->data;
446                         list = g_list_append(list, ci->fd);
447
448                         work = work->next;
449                         }
450                 g_free(path);
451                 collection_free(cd);
452                 }
453
454         return list;
455 }
456
457 /*
458  *-------------------------------------------------------------------
459  * please use these to actually add/remove stuff
460  *-------------------------------------------------------------------
461  */
462
463 CollectionData *collection_new(const gchar *path)
464 {
465         CollectionData *cd;
466         static gint untitled_counter = 0;
467
468         cd = g_new0(CollectionData, 1);
469
470         cd->ref = 1;    /* starts with a ref of 1 */
471         cd->sort_method = SORT_NONE;
472         cd->window_w = COLLECT_DEF_WIDTH;
473         cd->window_h = COLLECT_DEF_HEIGHT;
474         cd->existence = g_hash_table_new(NULL, NULL);
475
476         if (path)
477                 {
478                 cd->path = g_strdup(path);
479                 cd->name = g_strdup(filename_from_path(cd->path));
480                 /* load it */
481                 }
482         else
483                 {
484                 if (untitled_counter == 0)
485                         {
486                         cd->name = g_strdup(_("Untitled"));
487                         }
488                 else
489                         {
490                         cd->name = g_strdup_printf(_("Untitled (%d)"), untitled_counter + 1);
491                         }
492
493                 untitled_counter++;
494                 }
495
496         file_data_register_notify_func(collection_notify_cb, cd, NOTIFY_PRIORITY_MEDIUM);
497
498
499         collection_list = g_list_append(collection_list, cd);
500
501         return cd;
502 }
503
504 void collection_free(CollectionData *cd)
505 {
506         if (!cd) return;
507
508         DEBUG_1("collection \"%s\" freed", cd->name);
509
510         collection_load_stop(cd);
511         collection_list_free(cd->list);
512
513         file_data_unregister_notify_func(collection_notify_cb, cd);
514
515         collection_list = g_list_remove(collection_list, cd);
516
517         g_hash_table_destroy(cd->existence);
518
519         g_free(cd->path);
520         g_free(cd->name);
521
522         g_free(cd);
523 }
524
525 void collection_ref(CollectionData *cd)
526 {
527         cd->ref++;
528
529         DEBUG_1("collection \"%s\" ref count = %d", cd->name, cd->ref);
530 }
531
532 void collection_unref(CollectionData *cd)
533 {
534         cd->ref--;
535
536         DEBUG_1("collection \"%s\" ref count = %d", cd->name, cd->ref);
537
538         if (cd->ref < 1)
539                 {
540                 collection_free(cd);
541                 }
542 }
543
544 void collection_path_changed(CollectionData *cd)
545 {
546         collection_window_update_title(collection_window_find(cd));
547 }
548
549 gint collection_to_number(CollectionData *cd)
550 {
551         return g_list_index(collection_list, cd);
552 }
553
554 CollectionData *collection_from_number(gint n)
555 {
556         return g_list_nth_data(collection_list, n);
557 }
558
559 CollectionData *collection_from_dnd_data(const gchar *data, GList **list, GList **info_list)
560 {
561         CollectionData *cd;
562         gint collection_number;
563         const gchar *ptr;
564
565         if (list) *list = NULL;
566         if (info_list) *info_list = NULL;
567
568         if (strncmp(data, "COLLECTION:", 11) != 0) return NULL;
569
570         ptr = data + 11;
571
572         collection_number = atoi(ptr);
573         cd = collection_from_number(collection_number);
574         if (!cd) return NULL;
575
576         if (!list && !info_list) return cd;
577
578         while (*ptr != '\0' && *ptr != '\n' ) ptr++;
579         if (*ptr == '\0') return cd;
580         ptr++;
581
582         while (*ptr != '\0')
583                 {
584                 guint item_number;
585                 CollectInfo *info;
586
587                 item_number = (guint) atoi(ptr);
588                 while (*ptr != '\n' && *ptr != '\0') ptr++;
589                 if (*ptr == '\0')
590                         break;
591                 else
592                         while (*ptr == '\n') ptr++;
593
594                 info = g_list_nth_data(cd->list, item_number);
595                 if (!info) continue;
596
597                 if (list) *list = g_list_append(*list, file_data_ref(info->fd));
598                 if (info_list) *info_list = g_list_append(*info_list, info);
599                 }
600
601         return cd;
602 }
603
604 gchar *collection_info_list_to_dnd_data(CollectionData *cd, GList *list, gint *length)
605 {
606         GList *work;
607         GList *temp = NULL;
608         gchar *ptr;
609         gchar *text;
610         gchar *uri_text;
611         gint collection_number;
612
613         *length = 0;
614         if (!list) return NULL;
615
616         collection_number = collection_to_number(cd);
617         if (collection_number < 0) return NULL;
618
619         text = g_strdup_printf("COLLECTION:%d\n", collection_number);
620         *length += strlen(text);
621         temp = g_list_prepend(temp, text);
622
623         work = list;
624         while (work)
625                 {
626                 gint item_number = g_list_index(cd->list, work->data);
627
628                 work = work->next;
629
630                 if (item_number < 0) continue;
631
632                 text = g_strdup_printf("%d\n", item_number);
633                 temp = g_list_prepend(temp, text);
634                 *length += strlen(text);
635                 }
636
637         *length += 1; /* ending nul char */
638
639         uri_text = g_malloc(*length);
640         ptr = uri_text;
641
642         work = g_list_last(temp);
643         while (work)
644                 {
645                 gint len;
646                 gchar *text = work->data;
647
648                 work = work->prev;
649
650                 len = strlen(text);
651                 memcpy(ptr, text, len);
652                 ptr += len;
653                 }
654
655         ptr[0] = '\0';
656
657         string_list_free(temp);
658
659         return uri_text;
660 }
661
662 gint collection_info_valid(CollectionData *cd, CollectInfo *info)
663 {
664         if (collection_to_number(cd) < 0) return FALSE;
665
666         return (g_list_index(cd->list, info) != 0);
667 }
668
669 CollectInfo *collection_next_by_info(CollectionData *cd, CollectInfo *info)
670 {
671         GList *work;
672
673         work = g_list_find(cd->list, info);
674
675         if (!work) return NULL;
676         work = work->next;
677         if (work) return work->data;
678         return NULL;
679 }
680
681 CollectInfo *collection_prev_by_info(CollectionData *cd, CollectInfo *info)
682 {
683         GList *work;
684
685         work = g_list_find(cd->list, info);
686
687         if (!work) return NULL;
688         work = work->prev;
689         if (work) return work->data;
690         return NULL;
691 }
692
693 CollectInfo *collection_get_first(CollectionData *cd)
694 {
695         if (cd->list) return cd->list->data;
696
697         return NULL;
698 }
699
700 CollectInfo *collection_get_last(CollectionData *cd)
701 {
702         GList *list;
703
704         list = g_list_last(cd->list);
705
706         if (list) return list->data;
707
708         return NULL;
709 }
710
711 void collection_set_sort_method(CollectionData *cd, SortType method)
712 {
713         if (!cd) return;
714
715         if (cd->sort_method == method) return;
716
717         cd->sort_method = method;
718         cd->list = collection_list_sort(cd->list, cd->sort_method);
719         if (cd->list) cd->changed = TRUE;
720
721         collection_window_refresh(collection_window_find(cd));
722 }
723
724 void collection_randomize(CollectionData *cd)
725 {
726         if (!cd) return;
727
728         cd->list = collection_list_randomize(cd->list);
729         cd->sort_method = SORT_NONE;
730         if (cd->list) cd->changed = TRUE;
731
732         collection_window_refresh(collection_window_find(cd));
733 }
734
735 void collection_set_update_info_func(CollectionData *cd,
736                                      void (*func)(CollectionData *, CollectInfo *, gpointer), gpointer data)
737 {
738         cd->info_updated_func = func;
739         cd->info_updated_data = data;
740 }
741
742 static CollectInfo *collection_info_new_if_not_exists(CollectionData *cd, struct stat *st, FileData *fd)
743 {
744         CollectInfo *ci;
745
746         if (g_hash_table_lookup(cd->existence, fd->path)) return NULL;
747
748         ci = collection_info_new(fd, st, NULL);
749         if (ci) g_hash_table_insert(cd->existence, fd->path, "");
750         return ci;
751 }
752
753 gboolean collection_add_check(CollectionData *cd, FileData *fd, gboolean sorted, gboolean must_exist)
754 {
755         struct stat st;
756         gboolean valid;
757
758         if (!fd) return FALSE;
759
760         g_assert(fd->magick == FD_MAGICK);
761
762         if (must_exist)
763                 {
764                 valid = (stat_utf8(fd->path, &st) && !S_ISDIR(st.st_mode));
765                 }
766         else
767                 {
768                 valid = TRUE;
769                 st.st_size = 0;
770                 st.st_mtime = 0;
771                 }
772
773         if (valid)
774                 {
775                 CollectInfo *ci;
776
777                 ci = collection_info_new_if_not_exists(cd, &st, fd);
778                 if (!ci) return FALSE;
779                 DEBUG_3("add to collection: %s", fd->path);
780
781                 cd->list = collection_list_add(cd->list, ci, sorted ? cd->sort_method : SORT_NONE);
782                 cd->changed = TRUE;
783
784                 if (!sorted || cd->sort_method == SORT_NONE)
785                         {
786                         collection_window_add(collection_window_find(cd), ci);
787                         }
788                 else
789                         {
790                         collection_window_insert(collection_window_find(cd), ci);
791                         }
792                 }
793
794         return valid;
795 }
796
797 gboolean collection_add(CollectionData *cd, FileData *fd, gboolean sorted)
798 {
799         return collection_add_check(cd, fd, sorted, TRUE);
800 }
801
802 gboolean collection_insert(CollectionData *cd, FileData *fd, CollectInfo *insert_ci, gboolean sorted)
803 {
804         struct stat st;
805
806         if (!insert_ci) return collection_add(cd, fd, sorted);
807
808         if (stat_utf8(fd->path, &st) >= 0 && !S_ISDIR(st.st_mode))
809                 {
810                 CollectInfo *ci;
811
812                 ci = collection_info_new_if_not_exists(cd, &st, fd);
813                 if (!ci) return FALSE;
814
815                 DEBUG_3("insert in collection: %s", fd->path);
816
817                 cd->list = collection_list_insert(cd->list, ci, insert_ci, sorted ? cd->sort_method : SORT_NONE);
818                 cd->changed = TRUE;
819
820                 collection_window_insert(collection_window_find(cd), ci);
821
822                 return TRUE;
823                 }
824
825         return FALSE;
826 }
827
828 gboolean collection_remove(CollectionData *cd, FileData *fd)
829 {
830         CollectInfo *ci;
831
832         ci = collection_list_find_fd(cd->list, fd);
833
834         if (!ci) return FALSE;
835
836         g_hash_table_remove(cd->existence, fd->path);
837
838         cd->list = g_list_remove(cd->list, ci);
839         cd->changed = TRUE;
840
841         collection_window_remove(collection_window_find(cd), ci);
842         collection_info_free(ci);
843
844         return TRUE;
845 }
846
847 static void collection_remove_by_info(CollectionData *cd, CollectInfo *info)
848 {
849         if (!info || !g_list_find(cd->list, info)) return;
850
851         cd->list = g_list_remove(cd->list, info);
852         cd->changed = (cd->list != NULL);
853
854         collection_window_remove(collection_window_find(cd), info);
855         collection_info_free(info);
856 }
857
858 void collection_remove_by_info_list(CollectionData *cd, GList *list)
859 {
860         GList *work;
861
862         if (!list) return;
863
864         if (!list->next)
865                 {
866                 /* more efficient (in collect-table) to remove a single item this way */
867                 collection_remove_by_info(cd, (CollectInfo *)list->data);
868                 return;
869                 }
870
871         work = list;
872         while (work)
873                 {
874                 cd->list = collection_list_remove(cd->list, work->data);
875                 work = work->next;
876                 }
877         cd->changed = (cd->list != NULL);
878
879         collection_window_refresh(collection_window_find(cd));
880 }
881
882 gboolean collection_rename(CollectionData *cd, FileData *fd)
883 {
884         CollectInfo *ci;
885         ci = collection_list_find_fd(cd->list, fd);
886
887         if (!ci) return FALSE;
888
889         cd->changed = TRUE;
890
891         collection_window_update(collection_window_find(cd), ci);
892
893         return TRUE;
894 }
895
896 void collection_update_geometry(CollectionData *cd)
897 {
898         collection_window_get_geometry(collection_window_find(cd));
899 }
900
901 /*
902  *-------------------------------------------------------------------
903  * simple maintenance for renaming, deleting
904  *-------------------------------------------------------------------
905  */
906
907 static void collection_notify_cb(FileData *fd, NotifyType type, gpointer data)
908 {
909         CollectionData *cd = data;
910
911         if (!(type & NOTIFY_CHANGE) || !fd->change) return;
912
913         DEBUG_1("Notify collection: %s %04x", fd->path, type);
914
915         switch (fd->change->type)
916                 {
917                 case FILEDATA_CHANGE_MOVE:
918                 case FILEDATA_CHANGE_RENAME:
919                         collection_rename(cd, fd);
920                         break;
921                 case FILEDATA_CHANGE_COPY:
922                         break;
923                 case FILEDATA_CHANGE_DELETE:
924                         while (collection_remove(cd, fd));
925                         break;
926                 case FILEDATA_CHANGE_UNSPECIFIED:
927                 case FILEDATA_CHANGE_WRITE_METADATA:
928                         break;
929                 }
930
931 }
932
933
934 /*
935  *-------------------------------------------------------------------
936  * window key presses
937  *-------------------------------------------------------------------
938  */
939
940 static gboolean collection_window_keypress(GtkWidget *UNUSED(widget), GdkEventKey *event, gpointer data)
941 {
942         CollectWindow *cw = data;
943         gboolean stop_signal = FALSE;
944         GList *list;
945
946         if (event->state & GDK_CONTROL_MASK)
947                 {
948                 stop_signal = TRUE;
949                 switch (event->keyval)
950                         {
951                         case '1':
952                         case '2':
953                         case '3':
954                         case '4':
955                         case '5':
956                         case '6':
957                         case '7':
958                         case '8':
959                         case '9':
960                         case '0':
961                                 break;
962                         case 'A': case 'a':
963                                 if (event->state & GDK_SHIFT_MASK)
964                                         {
965                                         collection_table_unselect_all(cw->table);
966                                         }
967                                 else
968                                         {
969                                         collection_table_select_all(cw->table);
970                                         }
971                                 break;
972                         case 'L': case 'l':
973                                 list = layout_list(NULL);
974                                 if (list)
975                                         {
976                                         collection_table_add_filelist(cw->table, list);
977                                         filelist_free(list);
978                                         }
979                                 break;
980                         case 'C': case 'c':
981                                 file_util_copy(NULL, collection_table_selection_get_list(cw->table), NULL, cw->window);
982                                 break;
983                         case 'M': case 'm':
984                                 file_util_move(NULL, collection_table_selection_get_list(cw->table), NULL, cw->window);
985                                 break;
986                         case 'R': case 'r':
987                                 file_util_rename(NULL, collection_table_selection_get_list(cw->table), cw->window);
988                                 break;
989                         case 'D': case 'd':
990                                 options->file_ops.safe_delete_enable = TRUE;
991                                 file_util_delete(NULL, collection_table_selection_get_list(cw->table), cw->window);
992                                 break;
993                         case 'S': case 's':
994                                 collection_dialog_save_as(NULL, cw->cd);
995                                 break;
996                         case 'W': case 'w':
997                                 collection_window_close(cw);
998                                 break;
999                         default:
1000                                 stop_signal = FALSE;
1001                                 break;
1002                         }
1003                 }
1004         else
1005                 {
1006                 stop_signal = TRUE;
1007                 switch (event->keyval)
1008                         {
1009                         case GDK_KEY_Return: case GDK_KEY_KP_Enter:
1010                                 layout_image_set_collection(NULL, cw->cd,
1011                                         collection_table_get_focus_info(cw->table));
1012                                 break;
1013                         case 'V': case 'v':
1014                                 view_window_new_from_collection(cw->cd,
1015                                         collection_table_get_focus_info(cw->table));
1016                                 break;
1017                         case 'S': case 's':
1018                                 if (!cw->cd->path)
1019                                         {
1020                                         collection_dialog_save_as(NULL, cw->cd);
1021                                         }
1022                                 else if (!collection_save(cw->cd, cw->cd->path))
1023                                         {
1024                                         log_printf("failed saving to collection path: %s\n", cw->cd->path);
1025                                         }
1026                                 break;
1027                         case 'A': case 'a':
1028                                 collection_dialog_append(NULL, cw->cd);
1029                                 break;
1030                         case 'N': case 'n':
1031                                 collection_set_sort_method(cw->cd, SORT_NAME);
1032                                 break;
1033 #ifdef HAVE_STRVERSCMP
1034                         case 'I': case 'i':
1035                                 collection_set_sort_method(cw->cd, SORT_NUMBER);
1036                                 break;
1037 #endif
1038                         case 'D': case 'd':
1039                                 collection_set_sort_method(cw->cd, SORT_TIME);
1040                                 break;
1041                         case 'B': case 'b':
1042                                 collection_set_sort_method(cw->cd, SORT_SIZE);
1043                                 break;
1044                         case 'P': case 'p':
1045                                 if (event->state & GDK_SHIFT_MASK)
1046                                         {
1047                                         CollectInfo *info;
1048
1049                                         info = collection_table_get_focus_info(cw->table);
1050
1051                                         print_window_new(info->fd, collection_table_selection_get_list(cw->table),
1052                                                          collection_list_to_filelist(cw->cd->list), cw->window);
1053                                         }
1054                                 else
1055                                         {
1056                                         collection_set_sort_method(cw->cd, SORT_PATH);
1057                                         }
1058                                 break;
1059                         case 'R': case 'r':
1060                                 if (event->state & GDK_MOD1_MASK)
1061                                         {
1062                                                 options->collections.rectangular_selection = !(options->collections.rectangular_selection);
1063                                         }
1064                                 break;
1065                         case GDK_KEY_Delete: case GDK_KEY_KP_Delete:
1066                                 list = g_list_copy(cw->table->selection);
1067                                 if (list)
1068                                         {
1069                                         collection_remove_by_info_list(cw->cd, list);
1070                                         g_list_free(list);
1071                                         }
1072                                 else
1073                                         {
1074                                         collection_remove_by_info(cw->cd, collection_table_get_focus_info(cw->table));
1075                                         }
1076                                 break;
1077                         default:
1078                                 stop_signal = FALSE;
1079                                 break;
1080                         }
1081                 }
1082         if (!stop_signal && is_help_key(event))
1083                 {
1084                 help_window_show("GuideCollections.html");
1085                 stop_signal = TRUE;
1086                 }
1087
1088         return stop_signal;
1089 }
1090
1091 /*
1092  *-------------------------------------------------------------------
1093  * window
1094  *-------------------------------------------------------------------
1095  */
1096 static void collection_window_get_geometry(CollectWindow *cw)
1097 {
1098         CollectionData *cd;
1099         GdkWindow *window;
1100
1101         if (!cw) return;
1102
1103         cd = cw->cd;
1104         window = gtk_widget_get_window(cw->window);
1105         gdk_window_get_position(window, &cd->window_x, &cd->window_y);
1106         cd->window_w = gdk_window_get_width(window);
1107         cd->window_h = gdk_window_get_height(window);
1108         cd->window_read = TRUE;
1109 }
1110
1111 static void collection_window_refresh(CollectWindow *cw)
1112 {
1113         if (!cw) return;
1114
1115         collection_table_refresh(cw->table);
1116 }
1117
1118 static void collection_window_update_title(CollectWindow *cw)
1119 {
1120         gboolean free_name = FALSE;
1121         gchar *name;
1122         gchar *buf;
1123
1124         if (!cw) return;
1125
1126         if (file_extension_match(cw->cd->name, GQ_COLLECTION_EXT))
1127                 {
1128                 name = remove_extension_from_path(cw->cd->name);
1129                 free_name = TRUE;
1130                 }
1131         else
1132                 {
1133                 name = cw->cd->name;
1134                 }
1135
1136         buf = g_strdup_printf(_("%s - Collection - %s"), name, GQ_APPNAME);
1137         if (free_name) g_free(name);
1138         gtk_window_set_title(GTK_WINDOW(cw->window), buf);
1139         g_free(buf);
1140 }
1141
1142 static void collection_window_update_info(CollectionData *UNUSED(cd), CollectInfo *ci, gpointer data)
1143 {
1144         CollectWindow *cw = data;
1145
1146         collection_table_file_update(cw->table, ci);
1147 }
1148
1149 static void collection_window_add(CollectWindow *cw, CollectInfo *ci)
1150 {
1151         if (!cw) return;
1152
1153         if (!ci->pixbuf) collection_load_thumb_idle(cw->cd);
1154         collection_table_file_add(cw->table, ci);
1155 }
1156
1157 static void collection_window_insert(CollectWindow *cw, CollectInfo *ci)
1158 {
1159         if (!cw) return;
1160
1161         if (!ci->pixbuf) collection_load_thumb_idle(cw->cd);
1162         collection_table_file_insert(cw->table, ci);
1163         if (!cw) return;
1164 }
1165
1166 static void collection_window_remove(CollectWindow *cw, CollectInfo *ci)
1167 {
1168         if (!cw) return;
1169
1170         collection_table_file_remove(cw->table, ci);
1171 }
1172
1173 static void collection_window_update(CollectWindow *cw, CollectInfo *ci)
1174 {
1175         if (!cw) return;
1176
1177         collection_table_file_update(cw->table, ci);
1178         collection_table_file_update(cw->table, NULL);
1179 }
1180
1181 static void collection_window_close_final(CollectWindow *cw)
1182 {
1183         if (cw->close_dialog) return;
1184
1185         collection_window_list = g_list_remove(collection_window_list, cw);
1186         collection_window_get_geometry(cw);
1187
1188         gtk_widget_destroy(cw->window);
1189
1190         collection_set_update_info_func(cw->cd, NULL, NULL);
1191         collection_unref(cw->cd);
1192
1193         g_free(cw);
1194 }
1195
1196 static void collection_close_save_cb(GenericDialog *gd, gpointer data)
1197 {
1198         CollectWindow *cw = data;
1199
1200         cw->close_dialog = NULL;
1201         generic_dialog_close(gd);
1202
1203         if (!cw->cd->path)
1204                 {
1205                 collection_dialog_save_close(NULL, cw->cd);
1206                 return;
1207                 }
1208         else if (!collection_save(cw->cd, cw->cd->path))
1209                 {
1210                 gchar *buf;
1211                 buf = g_strdup_printf(_("Failed to save the collection:\n%s"), cw->cd->path);
1212                 warning_dialog(_("Save Failed"), buf, GTK_STOCK_DIALOG_ERROR, cw->window);
1213                 g_free(buf);
1214                 return;
1215                 }
1216
1217         collection_window_close_final(cw);
1218 }
1219
1220 static void collection_close_close_cb(GenericDialog *gd, gpointer data)
1221 {
1222         CollectWindow *cw = data;
1223
1224         cw->close_dialog = NULL;
1225         generic_dialog_close(gd);
1226
1227         collection_window_close_final(cw);
1228 }
1229
1230 static void collection_close_cancel_cb(GenericDialog *gd, gpointer data)
1231 {
1232         CollectWindow *cw = data;
1233
1234         cw->close_dialog = NULL;
1235         generic_dialog_close(gd);
1236 }
1237
1238 static void collection_close_dlg_show(CollectWindow *cw)
1239 {
1240         GenericDialog *gd;
1241
1242         if (cw->close_dialog)
1243                 {
1244                 gtk_window_present(GTK_WINDOW(cw->close_dialog));
1245                 return;
1246                 }
1247
1248         gd = generic_dialog_new(_("Close collection"),
1249                                 "close_collection", cw->window, FALSE,
1250                                 collection_close_cancel_cb, cw);
1251         generic_dialog_add_message(gd, GTK_STOCK_DIALOG_QUESTION,
1252                                    _("Close collection"),
1253                                    _("Collection has been modified.\nSave first?"), TRUE);
1254
1255         generic_dialog_add_button(gd, GTK_STOCK_SAVE, NULL, collection_close_save_cb, TRUE);
1256         generic_dialog_add_button(gd, GTK_STOCK_DELETE, _("_Discard"), collection_close_close_cb, FALSE);
1257
1258         cw->close_dialog = gd->dialog;
1259
1260         gtk_widget_show(gd->dialog);
1261 }
1262
1263 static void collection_window_close(CollectWindow *cw)
1264 {
1265         if (!cw->cd->changed && !cw->close_dialog)
1266                 {
1267                 collection_window_close_final(cw);
1268                 return;
1269                 }
1270
1271         collection_close_dlg_show(cw);
1272 }
1273
1274 void collection_window_close_by_collection(CollectionData *cd)
1275 {
1276         CollectWindow *cw;
1277
1278         cw = collection_window_find(cd);
1279         if (cw) collection_window_close_final(cw);
1280 }
1281
1282 /**
1283  * @brief Check if any Collection windows have unsaved data
1284  * @returns TRUE if unsaved data exists
1285  * 
1286  * Also saves window geometry for Collection windows that have
1287  * no unsaved data
1288  */
1289 gboolean collection_window_modified_exists(void)
1290 {
1291         GList *work;
1292         gboolean ret;
1293
1294         ret = FALSE;
1295
1296         work = collection_window_list;
1297         while (work)
1298                 {
1299                 CollectWindow *cw = work->data;
1300                 if (cw->cd->changed)
1301                         {
1302                         ret = TRUE;
1303                         }
1304                 else
1305                         {
1306                         if (!collection_save(cw->table->cd, cw->table->cd->path))
1307                                 {
1308                                 log_printf("failed saving to collection path: %s\n", cw->table->cd->path);
1309                                 }
1310                         }
1311                 work = work->next;
1312                 }
1313
1314         return ret;
1315 }
1316
1317 static gboolean collection_window_delete(GtkWidget *UNUSED(widget), GdkEvent *UNUSED(event), gpointer data)
1318 {
1319         CollectWindow *cw = data;
1320         collection_window_close(cw);
1321
1322         return TRUE;
1323 }
1324
1325 CollectWindow *collection_window_new(const gchar *path)
1326 {
1327         CollectWindow *cw;
1328         GtkWidget *vbox;
1329         GtkWidget *frame;
1330         GtkWidget *status_label;
1331         GtkWidget *extra_label;
1332         GdkGeometry geometry;
1333
1334         /* If the collection is already opened in another window, return that one */
1335         cw = collection_window_find_by_path(path);
1336         if (cw)
1337                 {
1338                 return cw;
1339                 }
1340
1341         cw = g_new0(CollectWindow, 1);
1342
1343         collection_window_list = g_list_append(collection_window_list, cw);
1344
1345         cw->cd = collection_new(path);
1346
1347         cw->window = window_new(GTK_WINDOW_TOPLEVEL, "collection", PIXBUF_INLINE_ICON_BOOK, NULL, NULL);
1348         DEBUG_NAME(cw->window);
1349
1350         geometry.min_width = DEFAULT_MINIMAL_WINDOW_SIZE;
1351         geometry.min_height = DEFAULT_MINIMAL_WINDOW_SIZE;
1352         geometry.base_width = COLLECT_DEF_WIDTH;
1353         geometry.base_height = COLLECT_DEF_HEIGHT;
1354         gtk_window_set_geometry_hints(GTK_WINDOW(cw->window), NULL, &geometry,
1355                                       GDK_HINT_MIN_SIZE | GDK_HINT_BASE_SIZE);
1356
1357         if (options->collections_on_top)
1358                 {
1359                 gtk_window_set_keep_above(GTK_WINDOW(cw->window), TRUE);
1360                 }
1361
1362         if (options->save_window_positions && path && collection_load_only_geometry(cw->cd, path))
1363                 {
1364                 gtk_window_set_default_size(GTK_WINDOW(cw->window), cw->cd->window_w, cw->cd->window_h);
1365                 gtk_window_move(GTK_WINDOW(cw->window), cw->cd->window_x, cw->cd->window_y);
1366                 }
1367         else
1368                 {
1369                 gtk_window_set_default_size(GTK_WINDOW(cw->window), COLLECT_DEF_WIDTH, COLLECT_DEF_HEIGHT);
1370                 }
1371
1372         gtk_window_set_resizable(GTK_WINDOW(cw->window), TRUE);
1373         collection_window_update_title(cw);
1374         gtk_container_set_border_width(GTK_CONTAINER(cw->window), 0);
1375
1376         g_signal_connect(G_OBJECT(cw->window), "delete_event",
1377                          G_CALLBACK(collection_window_delete), cw);
1378
1379         g_signal_connect(G_OBJECT(cw->window), "key_press_event",
1380                          G_CALLBACK(collection_window_keypress), cw);
1381
1382         vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
1383         gtk_container_add(GTK_CONTAINER(cw->window), vbox);
1384         gtk_widget_show(vbox);
1385
1386         cw->table = collection_table_new(cw->cd);
1387         gtk_box_pack_start(GTK_BOX(vbox), cw->table->scrolled, TRUE, TRUE, 0);
1388         gtk_widget_show(cw->table->scrolled);
1389
1390         cw->status_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
1391         gtk_box_pack_start(GTK_BOX(vbox), cw->status_box, FALSE, FALSE, 0);
1392         gtk_widget_show(cw->status_box);
1393
1394         frame = gtk_frame_new(NULL);
1395         DEBUG_NAME(frame);
1396         gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN);
1397         gtk_box_pack_start(GTK_BOX(cw->status_box), frame, TRUE, TRUE, 0);
1398         gtk_widget_show(frame);
1399
1400         status_label = gtk_label_new("");
1401         gtk_container_add(GTK_CONTAINER(frame), status_label);
1402         gtk_widget_show(status_label);
1403
1404         extra_label = gtk_progress_bar_new();
1405         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(extra_label), 0.0);
1406         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(extra_label), "");
1407         gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(extra_label), TRUE);
1408
1409         gtk_box_pack_start(GTK_BOX(cw->status_box), extra_label, TRUE, TRUE, 0);
1410         gtk_widget_show(extra_label);
1411
1412         collection_table_set_labels(cw->table, status_label, extra_label);
1413
1414         gtk_widget_show(cw->window);
1415         gtk_widget_grab_focus(cw->table->listview);
1416
1417         collection_set_update_info_func(cw->cd, collection_window_update_info, cw);
1418
1419         if (path && *path == G_DIR_SEPARATOR) collection_load_begin(cw->cd, NULL, COLLECTION_LOAD_NONE);
1420
1421         return cw;
1422 }
1423 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */