Move third-party sources to separate sub-directory
[geeqie.git] / src / collect-io.cc
1 /*
2  * Copyright (C) 2004 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-io.h"
23
24 #include <cstdio>
25 #include <cstring>
26
27 #include <gdk-pixbuf/gdk-pixbuf.h>
28 #include <gdk/gdk.h>
29 #include <glib-object.h>
30
31 #include <config.h>
32
33 #include "collect.h"
34 #include "debug.h"
35 #include "filedata.h"
36 #include "intl.h"
37 #include "layout-util.h"
38 #include "main-defines.h"
39 #include "options.h"
40 #include "secure-save.h"
41 #include "thumb.h"
42 #include "ui-fileops.h"
43
44 #define GQ_COLLECTION_MARKER "#" GQ_APPNAME
45
46 enum {
47         GQ_COLLECTION_FAIL_MIN =     300,
48         GQ_COLLECTION_FAIL_PERCENT = 98,
49         GQ_COLLECTION_READ_BUFSIZE = 4096
50 };
51
52 struct CollectManagerEntry;
53
54 static void collection_load_thumb_step(CollectionData *cd);
55 static gboolean collection_save_private(CollectionData *cd, const gchar *path);
56
57 static CollectManagerEntry *collect_manager_get_entry(const gchar *path);
58 static void collect_manager_entry_reset(CollectManagerEntry *entry);
59 static gint collect_manager_process_action(CollectManagerEntry *entry, gchar **path_ptr);
60
61 namespace
62 {
63
64 gboolean scan_geometry(gchar *buffer, GdkRectangle &window)
65 {
66         gint nx;
67         gint ny;
68         gint nw;
69         gint nh;
70
71         if (sscanf(buffer, "%d %d %d %d", &nx, &ny, &nw, &nh) != 4) return FALSE;
72
73         window.x = nx;
74         window.y = ny;
75         window.width = nw;
76         window.height = nh;
77
78         return TRUE;
79 }
80
81 }
82
83 static gboolean collection_load_private(CollectionData *cd, const gchar *path, CollectionLoadFlags flags)
84 {
85         gchar s_buf[GQ_COLLECTION_READ_BUFSIZE];
86         FILE *f;
87         gchar *pathl;
88         gboolean limit_failures = TRUE;
89         gboolean success = TRUE;
90         gboolean has_official_header = FALSE;
91         gboolean has_geometry_header = FALSE;
92         gboolean has_gqview_header   = FALSE;
93         gboolean need_header     = TRUE;
94         guint total = 0;
95         guint fail = 0;
96         gboolean changed = FALSE;
97         CollectManagerEntry *entry = nullptr;
98         guint flush = !!(flags & COLLECTION_LOAD_FLUSH);
99         guint append = !!(flags & COLLECTION_LOAD_APPEND);
100         guint only_geometry = !!(flags & COLLECTION_LOAD_GEOMETRY);
101         gboolean reading_extended_filename = FALSE;
102         gchar *buffer2;
103
104         if (!only_geometry)
105                 {
106                 collection_load_stop(cd);
107
108                 if (flush)
109                         collect_manager_flush();
110                 else
111                         entry = collect_manager_get_entry(path);
112
113                 if (!append)
114                         {
115                         g_list_free_full(cd->list, reinterpret_cast<GDestroyNotify>(collection_info_free));
116                         cd->list = nullptr;
117                         }
118                 }
119
120         if (!path && !cd->path) return FALSE;
121
122         if (!path) path = cd->path;
123
124         pathl = path_from_utf8(path);
125
126         DEBUG_1("collection load: append=%d flush=%d only_geometry=%d path=%s",
127                           append, flush, only_geometry, pathl);
128
129         /* load it */
130         f = fopen(pathl, "r");
131         g_free(pathl);
132         if (!f)
133                 {
134                 log_printf("Failed to open collection file: \"%s\"\n", path);
135                 return FALSE;
136                 }
137
138         GString *extended_filename_buffer = g_string_new(nullptr);
139         while (fgets(s_buf, sizeof(s_buf), f))
140                 {
141                 gchar *buf;
142                 gchar *p = s_buf;
143
144                 if (!reading_extended_filename)
145                         {
146                         /* Skip whitespaces and empty lines */
147                         while (*p && g_ascii_isspace(*p)) p++;
148                         if (*p == '\n' || *p == '\r') continue;
149
150                         /* Parse comments */
151                         if (*p == '#')
152                                 {
153                                 if (!need_header) continue;
154                                 if (g_ascii_strncasecmp(p, GQ_COLLECTION_MARKER, strlen(GQ_COLLECTION_MARKER)) == 0)
155                                         {
156                                         /* Looks like an official collection, allow unchecked input.
157                                          * All this does is allow adding files that may not exist,
158                                          * which is needed for the collection manager to work.
159                                          * Also unofficial files abort after too many invalid entries.
160                                          */
161                                         has_official_header = TRUE;
162                                         limit_failures = FALSE;
163                                         }
164                                 else if (strncmp(p, "#geometry:", 10 ) == 0 &&
165                                          scan_geometry(p + 10, cd->window))
166                                         {
167                                         has_geometry_header = TRUE;
168                                         cd->window_read = TRUE;
169                                         if (only_geometry) break;
170                                         }
171                                 else if (g_ascii_strncasecmp(p, "#GQview collection", strlen("#GQview collection")) == 0)
172                                         {
173                                         /* As 2008/04/15 there is no difference between our collection file format
174                                          * and GQview 2.1.5 collection file format so ignore failures as well. */
175                                         has_gqview_header = TRUE;
176                                         limit_failures = FALSE;
177                                         }
178                                 need_header = (!has_official_header && !has_gqview_header) || !has_geometry_header;
179                                 continue;
180                                 }
181
182                         if (only_geometry) continue;
183                         }
184
185                 /* Read filenames */
186                 /** @todo This is not safe! */
187                 /* Updated: anything within double quotes is considered a filename */
188                 if (!reading_extended_filename)
189                         {
190                         /* not yet reading filename */
191                         while (*p && *p != '"') p++;
192                         if (*p) p++;
193                         /* start of filename read */
194                         buf = p;
195                         while (*p && *p != '"') p++;
196                         if (p[0] != '"')
197                                 {
198                                 /* first part of extended filename */
199                                 g_string_append(extended_filename_buffer, buf);
200                                 reading_extended_filename = TRUE;
201                                 continue;
202                                 }
203                         }
204                 else
205                         {
206                         buf = p;
207                         while (*p && *p != '"') p++;
208                         if (p[0] != '"')
209                                 {
210                                 /* end of extended filename still not found */
211                                 g_string_append(extended_filename_buffer, buf);
212                                 continue;
213                                 }
214
215                         /* end of extended filename found */
216                         g_string_append_len(extended_filename_buffer, buf, p - buf);
217                         reading_extended_filename = FALSE;
218                         }
219
220                 if (extended_filename_buffer->len > 0)
221                         {
222                         buffer2 = g_strdup(extended_filename_buffer->str);
223                         g_string_erase(extended_filename_buffer, 0, -1);
224                         }
225                 else
226                         {
227                         *p = 0;
228                         buffer2 = g_strdup(buf);
229                         }
230
231                 if (*buffer2)
232                         {
233                         gboolean valid;
234
235                         if (!flush)
236                                 changed |= collect_manager_process_action(entry, &buffer2);
237
238                         valid = (buffer2[0] == G_DIR_SEPARATOR && collection_add_check(cd, file_data_new_simple(buffer2), FALSE, TRUE));
239                         if (!valid) DEBUG_1("collection invalid file: %s", buffer2);
240
241                         total++;
242                         if (!valid)
243                                 {
244                                 fail++;
245                                 if (limit_failures &&
246                                     fail > GQ_COLLECTION_FAIL_MIN &&
247                                     fail * 100 / total > GQ_COLLECTION_FAIL_PERCENT)
248                                         {
249                                         log_printf("%d invalid filenames in unofficial collection file, closing: %s\n", fail, path);
250                                         success = FALSE;
251                                         break;
252                                         }
253                                 }
254                         }
255                 g_free(buffer2);
256                 }
257
258         g_string_free(extended_filename_buffer, TRUE);
259
260         DEBUG_1("collection files: total = %d fail = %d official=%d gqview=%d geometry=%d",
261                           total, fail, has_official_header, has_gqview_header, has_geometry_header);
262
263         fclose(f);
264         if (only_geometry) return has_geometry_header;
265
266         if (!flush)
267                 {
268                 gchar *buf = nullptr;
269                 while (collect_manager_process_action(entry, &buf))
270                         {
271                         collection_add_check(cd, file_data_new_group(buf), FALSE, TRUE);
272                         changed = TRUE;
273                         g_free(buf);
274                         buf = nullptr;
275                         }
276                 }
277
278         cd->list = collection_list_sort(cd->list, cd->sort_method);
279
280         if (!flush && changed && success)
281                 collection_save_private(cd, path);
282
283         if (!flush)
284                 collect_manager_entry_reset(entry);
285
286         if (!append) cd->changed = FALSE;
287
288         return success;
289 }
290
291 gboolean collection_load(CollectionData *cd, const gchar *path, CollectionLoadFlags flags)
292 {
293         if (collection_load_private(cd, path, static_cast<CollectionLoadFlags>(flags | COLLECTION_LOAD_FLUSH)))
294                 {
295                 layout_recent_add_path(cd->path);
296                 return TRUE;
297                 }
298
299         return FALSE;
300 }
301
302 static void collection_load_thumb_do(CollectionData *cd)
303 {
304         GdkPixbuf *pixbuf;
305
306         if (!cd->thumb_loader || !g_list_find(cd->list, cd->thumb_info)) return;
307
308         pixbuf = thumb_loader_get_pixbuf(cd->thumb_loader);
309         collection_info_set_thumb(cd->thumb_info, pixbuf);
310         g_object_unref(pixbuf);
311
312         if (cd->info_updated_func) cd->info_updated_func(cd, cd->thumb_info, cd->info_updated_data);
313 }
314
315 static void collection_load_thumb_error_cb(ThumbLoader *, gpointer data)
316 {
317         auto cd = static_cast<CollectionData *>(data);
318
319         collection_load_thumb_do(cd);
320         collection_load_thumb_step(cd);
321 }
322
323 static void collection_load_thumb_done_cb(ThumbLoader *, gpointer data)
324 {
325         auto cd = static_cast<CollectionData *>(data);
326
327         collection_load_thumb_do(cd);
328         collection_load_thumb_step(cd);
329 }
330
331 static void collection_load_thumb_step(CollectionData *cd)
332 {
333         GList *work;
334         CollectInfo *ci;
335
336         if (!cd->list)
337                 {
338                 collection_load_stop(cd);
339                 return;
340                 }
341
342         work = cd->list;
343         ci = static_cast<CollectInfo *>(work->data);
344         work = work->next;
345         /* find first unloaded thumb */
346         while (work && ci->pixbuf)
347                 {
348                 ci = static_cast<CollectInfo *>(work->data);
349                 work = work->next;
350                 }
351
352         if (!ci || ci->pixbuf)
353                 {
354                 /* done */
355                 collection_load_stop(cd);
356
357                 /* send a NULL CollectInfo to notify end */
358                 if (cd->info_updated_func) cd->info_updated_func(cd, nullptr, cd->info_updated_data);
359
360                 return;
361                 }
362
363         /* setup loader and call it */
364         cd->thumb_info = ci;
365         thumb_loader_free(cd->thumb_loader);
366         cd->thumb_loader = thumb_loader_new(options->thumbnails.max_width, options->thumbnails.max_height);
367         thumb_loader_set_callbacks(cd->thumb_loader,
368                                    collection_load_thumb_done_cb,
369                                    collection_load_thumb_error_cb,
370                                    nullptr,
371                                    cd);
372
373         /* start it */
374         if (!thumb_loader_start(cd->thumb_loader, ci->fd))
375                 {
376                 /* error, handle it, do next */
377                 DEBUG_1("error loading thumb for %s", ci->fd->path);
378                 collection_load_thumb_do(cd);
379                 collection_load_thumb_step(cd);
380                 }
381 }
382
383 void collection_load_thumb_idle(CollectionData *cd)
384 {
385         if (!cd->thumb_loader) collection_load_thumb_step(cd);
386 }
387
388 gboolean collection_load_begin(CollectionData *cd, const gchar *path, CollectionLoadFlags flags)
389 {
390         if (!collection_load(cd, path, flags)) return FALSE;
391
392         collection_load_thumb_idle(cd);
393
394         return TRUE;
395 }
396
397 void collection_load_stop(CollectionData *cd)
398 {
399         if (!cd->thumb_loader) return;
400
401         thumb_loader_free(cd->thumb_loader);
402         cd->thumb_loader = nullptr;
403 }
404
405 static gboolean collection_save_private(CollectionData *cd, const gchar *path)
406 {
407         SecureSaveInfo *ssi;
408         GList *work;
409         gchar *pathl;
410
411         if (!path && !cd->path) return FALSE;
412
413         if (!path)
414                 {
415                 path = cd->path;
416                 }
417
418
419         pathl = path_from_utf8(path);
420         ssi = secure_open(pathl);
421         g_free(pathl);
422         if (!ssi)
423                 {
424                 log_printf(_("failed to open collection (write) \"%s\"\n"), path);
425                 return FALSE;
426                 }
427
428         secure_fprintf(ssi, "%s collection\n", GQ_COLLECTION_MARKER);
429         secure_fprintf(ssi, "#created with %s version %s\n", GQ_APPNAME, VERSION);
430
431         collection_update_geometry(cd);
432         if (cd->window_read)
433                 {
434                 secure_fprintf(ssi, "#geometry: %d %d %d %d\n", cd->window.x, cd->window.y, cd->window.width, cd->window.height);
435                 }
436
437         work = cd->list;
438         while (work && secsave_errno == SS_ERR_NONE)
439                 {
440                 auto ci = static_cast<CollectInfo *>(work->data);
441                 secure_fprintf(ssi, "\"%s\"\n", ci->fd->path);
442                 work = work->next;
443                 }
444
445         secure_fprintf(ssi, "#end\n");
446
447         if (secure_close(ssi))
448                 {
449                 log_printf(_("error saving collection file: %s\nerror: %s\n"), path,
450                             secsave_strerror(secsave_errno));
451                 return FALSE;
452                 }
453
454         if (!cd->path || strcmp(path, cd->path) != 0)
455                 {
456                 gchar *buf = cd->path;
457                 cd->path = g_strdup(path);
458                 path = cd->path;
459                 g_free(buf);
460
461                 g_free(cd->name);
462                 cd->name = g_strdup(filename_from_path(cd->path));
463
464                 collection_path_changed(cd);
465                 }
466
467         cd->changed = FALSE;
468
469         return TRUE;
470 }
471
472 gboolean collection_save(CollectionData *cd, const gchar *path)
473 {
474         if (collection_save_private(cd, path))
475                 {
476                 layout_recent_add_path(cd->path);
477                 return TRUE;
478                 }
479
480         return FALSE;
481 }
482
483 gboolean collection_load_only_geometry(CollectionData *cd, const gchar *path)
484 {
485         return collection_load(cd, path, COLLECTION_LOAD_GEOMETRY);
486 }
487
488
489 /*
490  *-------------------------------------------------------------------
491  * collection manager
492  *-------------------------------------------------------------------
493  */
494
495 enum {
496         COLLECT_MANAGER_ACTIONS_PER_IDLE = 1000,
497         COLLECT_MANAGER_FLUSH_DELAY =      10000
498 };
499
500 struct CollectManagerEntry
501 {
502         gchar *path;
503         GList *add_list;
504         GHashTable *oldpath_hash;
505         GHashTable *newpath_hash;
506         gboolean empty;
507 };
508
509 enum CollectManagerType {
510         COLLECTION_MANAGER_UPDATE,
511         COLLECTION_MANAGER_ADD,
512         COLLECTION_MANAGER_REMOVE
513 };
514
515 struct CollectManagerAction
516 {
517         gchar *oldpath;
518         gchar *newpath;
519
520         CollectManagerType type;
521
522         gint ref;
523 };
524
525
526 static GList *collection_manager_entry_list = nullptr;
527 static GList *collection_manager_action_list = nullptr;
528 static GList *collection_manager_action_tail = nullptr;
529 static guint collection_manager_timer_id = 0; /* event source id */
530
531
532 static CollectManagerAction *collect_manager_action_new(const gchar *oldpath, const gchar *newpath,
533                                                         CollectManagerType type)
534 {
535         CollectManagerAction *action;
536
537         action = g_new0(CollectManagerAction, 1);
538         action->ref = 1;
539
540         action->oldpath = g_strdup(oldpath);
541         action->newpath = g_strdup(newpath);
542
543         action->type = type;
544
545         return action;
546 }
547
548 static void collect_manager_action_ref(CollectManagerAction *action)
549 {
550         action->ref++;
551 }
552
553 static void collect_manager_action_unref(CollectManagerAction *action)
554 {
555         action->ref--;
556
557         if (action->ref > 0) return;
558
559         g_free(action->oldpath);
560         g_free(action->newpath);
561         g_free(action);
562 }
563
564 static void collect_manager_entry_free_data(CollectManagerEntry *entry)
565 {
566         g_list_free_full(entry->add_list, reinterpret_cast<GDestroyNotify>(collect_manager_action_unref));
567         if (g_hash_table_size(entry->oldpath_hash) > 0)
568                 g_hash_table_destroy(entry->oldpath_hash);
569         else
570                 g_hash_table_unref(entry->oldpath_hash);
571         if (g_hash_table_size(entry->newpath_hash) > 0)
572                 g_hash_table_destroy(entry->newpath_hash);
573         else
574                 g_hash_table_unref(entry->newpath_hash);
575 }
576
577 static void collect_manager_entry_init_data(CollectManagerEntry *entry)
578 {
579         entry->add_list = nullptr;
580         entry->oldpath_hash = g_hash_table_new_full(g_str_hash, g_str_equal, nullptr, reinterpret_cast<GDestroyNotify>(collect_manager_action_unref));
581         entry->newpath_hash = g_hash_table_new(g_str_hash, g_str_equal);
582         entry->empty = TRUE;
583
584 }
585
586 static CollectManagerEntry *collect_manager_entry_new(const gchar *path)
587 {
588         CollectManagerEntry *entry;
589
590         entry = g_new0(CollectManagerEntry, 1);
591         entry->path = g_strdup(path);
592         collect_manager_entry_init_data(entry);
593
594         collection_manager_entry_list = g_list_append(collection_manager_entry_list, entry);
595
596         return entry;
597 }
598
599
600 static void collect_manager_entry_free(CollectManagerEntry *entry)
601 {
602         collection_manager_entry_list = g_list_remove(collection_manager_entry_list, entry);
603
604         collect_manager_entry_free_data(entry);
605
606         g_free(entry->path);
607         g_free(entry);
608 }
609
610 static void collect_manager_entry_reset(CollectManagerEntry *entry)
611 {
612         collect_manager_entry_free_data(entry);
613         collect_manager_entry_init_data(entry);
614 }
615
616 static CollectManagerEntry *collect_manager_get_entry(const gchar *path)
617 {
618         GList *work;
619
620         work = collection_manager_entry_list;
621         while (work)
622                 {
623                 CollectManagerEntry *entry;
624
625                 entry = static_cast<CollectManagerEntry *>(work->data);
626                 work = work->next;
627                 if (strcmp(entry->path, path) == 0)
628                         {
629                         return entry;
630                         }
631                 }
632         return nullptr;
633
634 }
635
636 static void collect_manager_entry_add_action(CollectManagerEntry *entry, CollectManagerAction *action)
637 {
638
639         CollectManagerAction *orig_action;
640
641         entry->empty = FALSE;
642
643         if (action->oldpath == nullptr)
644                 {
645                 /* add file */
646                 if (action->newpath == nullptr)
647                         {
648                         return;
649                         }
650
651                 orig_action = static_cast<CollectManagerAction *>(g_hash_table_lookup(entry->newpath_hash, action->newpath));
652                 if (orig_action)
653                         {
654                         /* target already exists */
655                         log_printf("collection manager failed to add another action for target %s in collection %s\n",
656                                 action->newpath, entry->path);
657                         return;
658                         }
659                 entry->add_list = g_list_append(entry->add_list, action);
660                 g_hash_table_insert(entry->newpath_hash, action->newpath, action);
661                 collect_manager_action_ref(action);
662                 return;
663                 }
664
665         orig_action = static_cast<CollectManagerAction *>(g_hash_table_lookup(entry->newpath_hash, action->oldpath));
666         if (orig_action)
667                 {
668                 /* new action with the same file */
669                 CollectManagerAction *new_action = collect_manager_action_new(orig_action->oldpath, action->newpath, action->type);
670
671                 if (new_action->oldpath)
672                         {
673                         g_hash_table_steal(entry->oldpath_hash, orig_action->oldpath);
674                         g_hash_table_insert(entry->oldpath_hash, new_action->oldpath, new_action);
675                         }
676                 else
677                         {
678                         GList *work = g_list_find(entry->add_list, orig_action);
679                         work->data = new_action;
680                         }
681
682                 g_hash_table_steal(entry->newpath_hash, orig_action->newpath);
683                 if (new_action->newpath)
684                         {
685                         g_hash_table_insert(entry->newpath_hash, new_action->newpath, new_action);
686                         }
687                 collect_manager_action_unref(orig_action);
688                 return;
689                 }
690
691
692         orig_action = static_cast<CollectManagerAction *>(g_hash_table_lookup(entry->oldpath_hash, action->oldpath));
693         if (orig_action)
694                 {
695                 /* another action for the same source, ignore */
696                 log_printf("collection manager failed to add another action for source %s in collection %s\n",
697                         action->oldpath, entry->path);
698                 return;
699                 }
700
701         g_hash_table_insert(entry->oldpath_hash, action->oldpath, action);
702         if (action->newpath)
703                 {
704                 g_hash_table_insert(entry->newpath_hash, action->newpath, action);
705                 }
706         collect_manager_action_ref(action);
707 }
708
709 static gboolean collect_manager_process_action(CollectManagerEntry *entry, gchar **path_ptr)
710 {
711         gchar *path = *path_ptr;
712         CollectManagerAction *action;
713
714         if (path == nullptr)
715                 {
716                 /* get new files */
717                 if (entry->add_list)
718                         {
719                         action = static_cast<CollectManagerAction *>(entry->add_list->data);
720                         g_assert(action->oldpath == nullptr);
721                         entry->add_list = g_list_remove(entry->add_list, action);
722                         path = g_strdup(action->newpath);
723                         g_hash_table_remove(entry->newpath_hash, path);
724                         collect_manager_action_unref(action);
725                         }
726                 *path_ptr = path;
727                 return (path != nullptr);
728                 }
729
730         action = static_cast<CollectManagerAction *>(g_hash_table_lookup(entry->oldpath_hash, path));
731
732         if (action)
733                 {
734                 strcpy(*path_ptr, action->newpath);
735                 return TRUE;
736                 }
737
738         return FALSE; /* no change */
739 }
740
741 static void collect_manager_refresh()
742 {
743         GList *list;
744         GList *work;
745         FileData *dir_fd;
746
747         dir_fd = file_data_new_dir(get_collections_dir());
748         filelist_read(dir_fd, &list, nullptr);
749         file_data_unref(dir_fd);
750
751         work = collection_manager_entry_list;
752         while (work && list)
753                 {
754                 CollectManagerEntry *entry;
755                 GList *list_step;
756
757                 entry = static_cast<CollectManagerEntry *>(work->data);
758                 work = work->next;
759
760                 list_step = list;
761                 while (list_step && entry)
762                         {
763                         FileData *fd;
764
765                         fd = static_cast<FileData *>(list_step->data);
766                         list_step = list_step->next;
767
768                         if (strcmp(fd->path, entry->path) == 0)
769                                 {
770                                 list = g_list_remove(list, fd);
771                                 file_data_unref(fd);
772
773                                 entry = nullptr;
774                                 }
775                         else
776                                 {
777                                 collect_manager_entry_free(entry);
778
779                                 entry = nullptr;
780                                 }
781                         }
782                 }
783
784         work = list;
785         while (work)
786                 {
787                 FileData *fd;
788
789                 fd = static_cast<FileData *>(work->data);
790                 work = work->next;
791
792                 collect_manager_entry_new(fd->path);
793                 }
794
795         filelist_free(list);
796 }
797
798 static void collect_manager_process_actions(gint max)
799 {
800         if (collection_manager_action_list) DEBUG_1("collection manager processing actions");
801
802         while (collection_manager_action_list != nullptr && max > 0)
803                 {
804                 CollectManagerAction *action;
805                 GList *work;
806
807                 action = static_cast<CollectManagerAction *>(collection_manager_action_list->data);
808                 work = collection_manager_entry_list;
809                 while (work)
810                         {
811                         CollectManagerEntry *entry;
812
813                         entry = static_cast<CollectManagerEntry *>(work->data);
814                         work = work->next;
815
816                         if (action->type == COLLECTION_MANAGER_UPDATE)
817                                 {
818                                 collect_manager_entry_add_action(entry, action);
819                                 }
820                         else if (action->oldpath && action->newpath &&
821                                  strcmp(action->newpath, entry->path) == 0)
822                                 {
823                                 /* convert action to standard add format */
824                                 g_free(action->newpath);
825                                 if (action->type == COLLECTION_MANAGER_ADD)
826                                         {
827                                         action->newpath = action->oldpath;
828                                         action->oldpath = nullptr;
829                                         }
830                                 else if (action->type == COLLECTION_MANAGER_REMOVE)
831                                         {
832                                         action->newpath = nullptr;
833                                         }
834                                 collect_manager_entry_add_action(entry, action);
835                                 }
836
837                         max--;
838                         }
839
840                 if (action->type != COLLECTION_MANAGER_UPDATE &&
841                     action->oldpath && action->newpath)
842                         {
843                         log_printf("collection manager failed to %s %s for collection %s\n",
844                                 (action->type == COLLECTION_MANAGER_ADD) ? "add" : "remove",
845                                 action->oldpath, action->newpath);
846                         }
847
848                 if (collection_manager_action_tail == collection_manager_action_list)
849                         {
850                         collection_manager_action_tail = nullptr;
851                         }
852                 collection_manager_action_list = g_list_remove(collection_manager_action_list, action);
853                 collect_manager_action_unref(action);
854                 }
855 }
856
857 static gboolean collect_manager_process_entry(CollectManagerEntry *entry)
858 {
859         CollectionData *cd;
860
861         if (entry->empty) return FALSE;
862
863         cd = collection_new(entry->path);
864         (void) collection_load_private(cd, entry->path, COLLECTION_LOAD_NONE);
865
866         collection_unref(cd);
867
868         return TRUE;
869 }
870
871 static gboolean collect_manager_process_entry_list()
872 {
873         GList *work;
874
875         work = collection_manager_entry_list;
876         while (work)
877                 {
878                 CollectManagerEntry *entry;
879
880                 entry = static_cast<CollectManagerEntry *>(work->data);
881                 work = work->next;
882                 if (collect_manager_process_entry(entry)) return TRUE;
883                 }
884
885         return FALSE;
886 }
887
888
889
890 static gboolean collect_manager_process_cb(gpointer)
891 {
892         if (collection_manager_action_list) collect_manager_refresh();
893         collect_manager_process_actions(COLLECT_MANAGER_ACTIONS_PER_IDLE);
894         if (collection_manager_action_list) return G_SOURCE_CONTINUE;
895
896         if (collect_manager_process_entry_list()) return G_SOURCE_CONTINUE;
897
898         DEBUG_1("collection manager is up to date");
899         return G_SOURCE_REMOVE;
900 }
901
902 static gboolean collect_manager_timer_cb(gpointer)
903 {
904         DEBUG_1("collection manager timer expired");
905
906         g_idle_add_full(G_PRIORITY_LOW, collect_manager_process_cb, nullptr, nullptr);
907
908         collection_manager_timer_id = 0;
909         return FALSE;
910 }
911
912 static void collect_manager_timer_push(gint stop)
913 {
914         if (collection_manager_timer_id)
915                 {
916                 if (!stop) return;
917
918                 g_source_remove(collection_manager_timer_id);
919                 collection_manager_timer_id = 0;
920                 }
921
922         if (!stop)
923                 {
924                 collection_manager_timer_id = g_timeout_add(COLLECT_MANAGER_FLUSH_DELAY,
925                                                             collect_manager_timer_cb, nullptr);
926                 DEBUG_1("collection manager timer started");
927                 }
928 }
929
930 static void collect_manager_add_action(CollectManagerAction *action)
931 {
932         if (!action) return;
933
934         /* we keep track of the list's tail to keep this a n(1) operation */
935
936         if (collection_manager_action_tail)
937                 {
938                 collection_manager_action_tail = g_list_append(collection_manager_action_tail, action);
939                 collection_manager_action_tail = collection_manager_action_tail->next;
940                 }
941         else
942                 {
943                 collection_manager_action_list = g_list_append(collection_manager_action_list, action);
944                 collection_manager_action_tail = collection_manager_action_list;
945                 }
946
947         collect_manager_timer_push(FALSE);
948 }
949
950 void collect_manager_moved(FileData *fd)
951 {
952         CollectManagerAction *action;
953         const gchar *oldpath = fd->change->source;
954         const gchar *newpath = fd->change->dest;
955
956         action = collect_manager_action_new(oldpath, newpath, COLLECTION_MANAGER_UPDATE);
957         collect_manager_add_action(action);
958 }
959
960 void collect_manager_add(FileData *fd, const gchar *collection)
961 {
962         CollectManagerAction *action;
963         CollectWindow *cw;
964
965         if (!fd || !collection) return;
966
967         cw = collection_window_find_by_path(collection);
968         if (cw)
969                 {
970                 if (collection_list_find_fd(cw->cd->list, fd) == nullptr)
971                         {
972                         collection_add(cw->cd, fd, FALSE);
973                         }
974                 return;
975                 }
976
977         action = collect_manager_action_new(fd->path, collection, COLLECTION_MANAGER_ADD);
978         collect_manager_add_action(action);
979 }
980
981 void collect_manager_remove(FileData *fd, const gchar *collection)
982 {
983         CollectManagerAction *action;
984         CollectWindow *cw;
985
986         if (!fd || !collection) return;
987
988         cw = collection_window_find_by_path(collection);
989         if (cw)
990                 {
991                 while (collection_remove(cw->cd, fd));
992                 return;
993                 }
994
995         action = collect_manager_action_new(fd->path, collection, COLLECTION_MANAGER_REMOVE);
996         collect_manager_add_action(action);
997 }
998
999 void collect_manager_flush()
1000 {
1001         collect_manager_timer_push(TRUE);
1002
1003         DEBUG_1("collection manager flushing");
1004         while (collect_manager_process_cb(nullptr));
1005 }
1006
1007 void collect_manager_notify_cb(FileData *fd, NotifyType type, gpointer)
1008 {
1009         if (!(type & NOTIFY_CHANGE) || !fd->change) return;
1010
1011         DEBUG_1("Notify collect_manager: %s %04x", fd->path, type);
1012         switch (fd->change->type)
1013                 {
1014                 case FILEDATA_CHANGE_MOVE:
1015                         collect_manager_moved(fd);
1016                         break;
1017                 case FILEDATA_CHANGE_COPY:
1018                         break;
1019                 case FILEDATA_CHANGE_RENAME:
1020                         collect_manager_moved(fd);
1021                         break;
1022                 case FILEDATA_CHANGE_DELETE:
1023                 case FILEDATA_CHANGE_UNSPECIFIED:
1024                 case FILEDATA_CHANGE_WRITE_METADATA:
1025                         break;
1026                 }
1027 }
1028
1029 static gint collection_manager_sort_cb(gconstpointer a, gconstpointer b)
1030 {
1031         auto char_a = static_cast<const gchar *>(a);
1032         auto char_b = static_cast<const gchar *>(b);
1033
1034         return g_strcmp0(char_a, char_b);
1035 }
1036
1037 /**
1038  * @brief Creates sorted list of collections
1039  * @param[out] names_exc sorted list of collections names excluding extension
1040  * @param[out] names_inc sorted list of collections names including extension
1041  * @param[out] paths sorted list of collection paths
1042  *
1043  * Lists of type gchar.
1044  * Used lists must be freed with string_list_free()
1045  */
1046 void collect_manager_list(GList **names_exc, GList **names_inc, GList **paths)
1047 {
1048         FileData *dir_fd;
1049         GList *list = nullptr;
1050         gchar *name;
1051         FileData *fd;
1052         gchar *filename;
1053
1054         if (names_exc == nullptr && names_inc == nullptr && paths == nullptr)
1055                 {
1056                 return;
1057                 }
1058
1059         dir_fd = file_data_new_dir((get_collections_dir()));
1060
1061         filelist_read(dir_fd, &list, nullptr);
1062
1063         while (list)
1064                 {
1065                 fd = static_cast<FileData *>(list->data);
1066                 filename = g_strdup(filename_from_path(fd->path));
1067
1068                 if (file_extension_match(filename, GQ_COLLECTION_EXT))
1069                         {
1070                         name = remove_extension_from_path(filename);
1071
1072                         if (names_exc != nullptr)
1073                                 {
1074                                 *names_exc = g_list_insert_sorted(*names_exc, g_strdup(name),
1075                                                                                         collection_manager_sort_cb);
1076                                 *names_exc = g_list_first(*names_exc);
1077                                 }
1078                         if (names_inc != nullptr)
1079                                 {
1080                                 *names_inc = g_list_insert_sorted(*names_inc,filename,
1081                                                                                         collection_manager_sort_cb);
1082                                 *names_inc = g_list_first(*names_inc);
1083                                 }
1084                         if (paths != nullptr)
1085                                 {
1086                                 *paths = g_list_insert_sorted(*paths, g_strdup(fd->path),
1087                                                                                         collection_manager_sort_cb);
1088                                 *paths = g_list_first(*paths);
1089                                 }
1090                         g_free(name);
1091                         }
1092                 list = list->next;
1093                 g_free(filename);
1094                 }
1095
1096         filelist_free(list);
1097 }
1098 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */