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