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