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