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