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