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