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