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