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