7c8799bece9af63f0520e624ef352e819e86e361
[geeqie.git] / src / collect-table.cc
1 /*
2  * Copyright (C) 2004 John Ellis
3  * Copyright (C) 2008 - 2016 The Geeqie Team
4  *
5  * Author: John Ellis
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 #include "main.h"
23 #include "collect-table.h"
24
25 #include "cellrenderericon.h"
26 #include "collect-dlg.h"
27 #include "collect-io.h"
28 #include "dnd.h"
29 #include "dupe.h"
30 #include "filedata.h"
31 #include "img-view.h"
32 #include "layout.h"
33 #include "layout-image.h"
34 #include "menu.h"
35 #include "metadata.h"
36 #include "misc.h"
37 #include "pixbuf-util.h"
38 #include "print.h"
39 #include "utilops.h"
40 #include "ui-fileops.h"
41 #include "ui-menu.h"
42 #include "ui-tree-edit.h"
43 #include "uri-utils.h"
44 #include "view-file.h"
45
46 /* between these, the icon width is increased by thumb_max_width / 2 */
47 enum {
48         THUMB_MIN_ICON_WIDTH = 128,
49         THUMB_MAX_ICON_WIDTH = 150
50 };
51
52 enum {
53         COLLECT_TABLE_MAX_COLUMNS = 32,
54         THUMB_BORDER_PADDING = 2
55 };
56
57 enum {
58         COLLECT_TABLE_TIP_DELAY = 500,
59         COLLECT_TABLE_TIP_DELAY_PATH = 850
60 };
61
62 enum {
63         CTABLE_COLUMN_POINTER = 0,
64         CTABLE_COLUMN_COUNT
65 };
66
67 #define INFO_SELECTED(x) ((x)->flag_mask & SELECTION_SELECTED)
68
69
70 static void collection_table_populate_at_new_size(CollectTable *ct, gint w, gint h, gboolean force);
71
72 /**
73  * This array must be kept in sync with the contents of:\n
74  * @link collection_table_press_key_cb @endlink \n
75  * @link collection_window_keypress @endlink \n
76  * @link collection_table_popup_menu @endlink
77  *
78  * See also @link hard_coded_window_keys @endlink
79  **/
80 hard_coded_window_keys collection_window_keys[] = {
81         {GDK_CONTROL_MASK, 'C', N_("Copy")},
82         {GDK_CONTROL_MASK, 'M', N_("Move")},
83         {GDK_CONTROL_MASK, 'R', N_("Rename")},
84         {GDK_CONTROL_MASK, 'D', N_("Move to Trash")},
85         {GDK_CONTROL_MASK, 'W', N_("Close window")},
86         {static_cast<GdkModifierType>(0), GDK_KEY_Delete, N_("Remove")},
87         {static_cast<GdkModifierType>(0), GDK_KEY_Return, N_("View")},
88         {static_cast<GdkModifierType>(0), 'V', N_("View in new window")},
89         {GDK_CONTROL_MASK, 'A', N_("Select all")},
90         {static_cast<GdkModifierType>(GDK_CONTROL_MASK + GDK_SHIFT_MASK), 'A', N_("Select none")},
91         {GDK_MOD1_MASK, 'R', N_("Rectangular selection")},
92         {static_cast<GdkModifierType>(0), GDK_KEY_space, N_("Select single file")},
93         {GDK_CONTROL_MASK, GDK_KEY_space, N_("Toggle select image")},
94         {GDK_CONTROL_MASK, 'L', N_("Append from file selection")},
95         {static_cast<GdkModifierType>(0), 'A', N_("Append from collection")},
96         {static_cast<GdkModifierType>(0), 'S', N_("Save collection")},
97         {GDK_CONTROL_MASK, 'S', N_("Save collection as")},
98         {GDK_CONTROL_MASK, 'T', N_("Show filename text")},
99         {static_cast<GdkModifierType>(0), 'N', N_("Sort by name")},
100         {static_cast<GdkModifierType>(0), 'D', N_("Sort by date")},
101         {static_cast<GdkModifierType>(0), 'B', N_("Sort by size")},
102         {static_cast<GdkModifierType>(0), 'P', N_("Sort by path")},
103         {GDK_SHIFT_MASK, 'P', N_("Print")},
104         {GDK_MOD1_MASK, 'A', N_("Append (Append collection dialog)")},
105         {GDK_MOD1_MASK, 'D', N_("Discard (Close modified collection dialog)")},
106         {static_cast<GdkModifierType>(0), 0, nullptr}
107 };
108
109 /*
110  *-------------------------------------------------------------------
111  * more misc
112  *-------------------------------------------------------------------
113  */
114
115 static gboolean collection_table_find_position(CollectTable *ct, CollectInfo *info, gint *row, gint *col)
116 {
117         gint n;
118
119         n = g_list_index(ct->cd->list, info);
120
121         if (n < 0) return FALSE;
122
123         *row = n / ct->columns;
124         *col = n - (*row * ct->columns);
125
126         return TRUE;
127 }
128
129 static gboolean collection_table_find_iter(CollectTable *ct, CollectInfo *info, GtkTreeIter *iter, gint *column)
130 {
131         GtkTreeModel *store;
132         gint row;
133         gint col;
134
135         store = gtk_tree_view_get_model(GTK_TREE_VIEW(ct->listview));
136         if (!collection_table_find_position(ct, info, &row, &col)) return FALSE;
137         if (!gtk_tree_model_iter_nth_child(store, iter, nullptr, row)) return FALSE;
138         if (column) *column = col;
139
140         return TRUE;
141 }
142
143 static CollectInfo *collection_table_find_data(CollectTable *ct, gint row, gint col, GtkTreeIter *iter)
144 {
145         GtkTreeModel *store;
146         GtkTreeIter p;
147
148         if (row < 0 || col < 0) return nullptr;
149
150         store = gtk_tree_view_get_model(GTK_TREE_VIEW(ct->listview));
151         if (gtk_tree_model_iter_nth_child(store, &p, nullptr, row))
152                 {
153                 GList *list;
154
155                 gtk_tree_model_get(store, &p, CTABLE_COLUMN_POINTER, &list, -1);
156                 if (!list) return nullptr;
157
158                 if (iter) *iter = p;
159
160                 return static_cast<CollectInfo *>(g_list_nth_data(list, col));
161                 }
162
163         return nullptr;
164 }
165
166 static CollectInfo *collection_table_find_data_by_coord(CollectTable *ct, gint x, gint y, GtkTreeIter *iter)
167 {
168         GtkTreePath *tpath;
169         GtkTreeViewColumn *column;
170         GtkTreeModel *store;
171         GtkTreeIter row;
172         GList *list;
173         gint n;
174
175         if (!gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(ct->listview), x, y,
176                                            &tpath, &column, nullptr, nullptr))
177                 return nullptr;
178
179         store = gtk_tree_view_get_model(GTK_TREE_VIEW(ct->listview));
180         gtk_tree_model_get_iter(store, &row, tpath);
181         gtk_tree_path_free(tpath);
182
183         gtk_tree_model_get(store, &row, CTABLE_COLUMN_POINTER, &list, -1);
184         if (!list) return nullptr;
185
186         n = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(column), "column_number"));
187         if (iter) *iter = row;
188         return static_cast<CollectInfo *>(g_list_nth_data(list, n));
189 }
190
191 static guint collection_table_list_count(CollectTable *ct, gint64 *bytes)
192 {
193         if (bytes)
194                 {
195                 gint64 b = 0;
196                 GList *work;
197
198                 work = ct->cd->list;
199                 while (work)
200                         {
201                         auto ci = static_cast<CollectInfo *>(work->data);
202                         work = work->next;
203                         b += ci->fd->size;
204                         }
205
206                 *bytes = b;
207                 }
208
209         return g_list_length(ct->cd->list);
210 }
211
212 static guint collection_table_selection_count(CollectTable *ct, gint64 *bytes)
213 {
214         if (bytes)
215                 {
216                 gint64 b = 0;
217                 GList *work;
218
219                 work = ct->selection;
220                 while (work)
221                         {
222                         auto ci = static_cast<CollectInfo *>(work->data);
223                         work = work->next;
224                         b += ci->fd->size;
225                         }
226
227                 *bytes = b;
228                 }
229
230         return g_list_length(ct->selection);
231 }
232
233 static void collection_table_update_status(CollectTable *ct)
234 {
235         gchar *buf;
236         guint n;
237         gint64 n_bytes = 0;
238         guint s;
239         gint64 s_bytes = 0;
240
241         if (!ct->status_label) return;
242
243         n = collection_table_list_count(ct, &n_bytes);
244         s = collection_table_selection_count(ct, &s_bytes);
245
246         if (s > 0)
247                 {
248                 gchar *b = text_from_size_abrev(n_bytes);
249                 gchar *sb = text_from_size_abrev(s_bytes);
250                 buf = g_strdup_printf(_("%s, %d images (%s, %d)"), b, n, sb, s);
251                 g_free(b);
252                 g_free(sb);
253                 }
254         else if (n > 0)
255                 {
256                 gchar *b = text_from_size_abrev(n_bytes);
257                 buf = g_strdup_printf(_("%s, %d images"), b, n);
258                 g_free(b);
259                 }
260         else
261                 {
262                 buf = g_strdup(_("Empty"));
263                 }
264
265         gtk_label_set_text(GTK_LABEL(ct->status_label), buf);
266         g_free(buf);
267 }
268
269 static void collection_table_update_extras(CollectTable *ct, gboolean loading, gdouble value)
270 {
271         const gchar *text;
272
273         if (!ct->extra_label) return;
274
275         if (loading)
276                 text = _("Loading thumbs...");
277         else
278                 text = " ";
279
280         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ct->extra_label), value);
281         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ct->extra_label), text);
282 }
283
284 static void collection_table_toggle_filenames(CollectTable *ct)
285 {
286         GtkAllocation allocation;
287         ct->show_text = !ct->show_text;
288         options->show_icon_names = ct->show_text;
289
290         gtk_widget_get_allocation(ct->listview, &allocation);
291         collection_table_populate_at_new_size(ct, allocation.width, allocation.height, TRUE);
292 }
293
294 static void collection_table_toggle_stars(CollectTable *ct)
295 {
296         GtkAllocation allocation;
297         ct->show_stars = !ct->show_stars;
298         options->show_star_rating = ct->show_stars;
299
300         gtk_widget_get_allocation(ct->listview, &allocation);
301         collection_table_populate_at_new_size(ct, allocation.width, allocation.height, TRUE);
302 }
303
304 static gint collection_table_get_icon_width(CollectTable *ct)
305 {
306         gint width;
307
308         if (!ct->show_text) return options->thumbnails.max_width;
309
310         width = options->thumbnails.max_width + options->thumbnails.max_width / 2;
311         if (width < THUMB_MIN_ICON_WIDTH) width = THUMB_MIN_ICON_WIDTH;
312         if (width > THUMB_MAX_ICON_WIDTH) width = options->thumbnails.max_width;
313
314         return width;
315 }
316
317 /*
318  *-------------------------------------------------------------------
319  * cell updates
320  *-------------------------------------------------------------------
321  */
322
323 static void collection_table_selection_set(CollectTable *ct, CollectInfo *info, SelectionType value, GtkTreeIter *iter)
324 {
325         GtkTreeModel *store;
326         GList *list;
327
328         if (!info) return;
329
330         if (info->flag_mask == value) return;
331         info->flag_mask = value;
332
333         store = gtk_tree_view_get_model(GTK_TREE_VIEW(ct->listview));
334         if (iter)
335                 {
336                 gtk_tree_model_get(store, iter, CTABLE_COLUMN_POINTER, &list, -1);
337                 if (list) gtk_list_store_set(GTK_LIST_STORE(store), iter, CTABLE_COLUMN_POINTER, list, -1);
338                 }
339         else
340                 {
341                 GtkTreeIter row;
342
343                 if (collection_table_find_iter(ct, info, &row, nullptr))
344                         {
345                         gtk_tree_model_get(store, &row, CTABLE_COLUMN_POINTER, &list, -1);
346                         if (list) gtk_list_store_set(GTK_LIST_STORE(store), &row, CTABLE_COLUMN_POINTER, list, -1);
347                         }
348                 }
349 }
350
351 static void collection_table_selection_add(CollectTable *ct, CollectInfo *info, SelectionType mask, GtkTreeIter *iter)
352 {
353         if (!info) return;
354
355         collection_table_selection_set(ct, info, static_cast<SelectionType>(info->flag_mask | mask), iter);
356 }
357
358 static void collection_table_selection_remove(CollectTable *ct, CollectInfo *info, SelectionType mask, GtkTreeIter *iter)
359 {
360         if (!info) return;
361
362         collection_table_selection_set(ct, info, static_cast<SelectionType>(info->flag_mask & ~mask), iter);
363 }
364 /*
365  *-------------------------------------------------------------------
366  * selections
367  *-------------------------------------------------------------------
368  */
369
370 static void collection_table_verify_selections(CollectTable *ct)
371 {
372         GList *work;
373
374         work = ct->selection;
375         while (work)
376                 {
377                 auto info = static_cast<CollectInfo *>(work->data);
378                 work = work->next;
379                 if (!g_list_find(ct->cd->list, info))
380                         {
381                         ct->selection = g_list_remove(ct->selection, info);
382                         }
383                 }
384 }
385
386 void collection_table_select_all(CollectTable *ct)
387 {
388         GList *work;
389
390         g_list_free(ct->selection);
391         ct->selection = nullptr;
392
393         work = ct->cd->list;
394         while (work)
395                 {
396                 ct->selection = g_list_append(ct->selection, work->data);
397                 collection_table_selection_add(ct, static_cast<CollectInfo *>(work->data), SELECTION_SELECTED, nullptr);
398                 work = work->next;
399                 }
400
401         collection_table_update_status(ct);
402 }
403
404 void collection_table_unselect_all(CollectTable *ct)
405 {
406         GList *work;
407
408         work = ct->selection;
409         while (work)
410                 {
411                 collection_table_selection_remove(ct, static_cast<CollectInfo *>(work->data), SELECTION_SELECTED, nullptr);
412                 work = work->next;
413                 }
414
415         g_list_free(ct->selection);
416         ct->selection = nullptr;
417
418         collection_table_update_status(ct);
419 }
420
421 /* Invert the current collection's selection */
422 static void collection_table_select_invert_all(CollectTable *ct)
423 {
424         GList *work;
425         GList *new_selection = nullptr;
426
427         work = ct->cd->list;
428         while (work)
429                 {
430                 auto info = static_cast<CollectInfo *>(work->data);
431
432                 if (INFO_SELECTED(info))
433                         {
434                         collection_table_selection_remove(ct, info, SELECTION_SELECTED, nullptr);
435                         }
436                 else
437                         {
438                         new_selection = g_list_append(new_selection, info);
439                         collection_table_selection_add(ct, info, SELECTION_SELECTED, nullptr);
440
441                         }
442
443                 work = work->next;
444                 }
445
446         g_list_free(ct->selection);
447         ct->selection = new_selection;
448
449         collection_table_update_status(ct);
450 }
451
452 void collection_table_select(CollectTable *ct, CollectInfo *info)
453 {
454         ct->prev_selection = info;
455
456         if (!info || INFO_SELECTED(info)) return;
457
458         ct->selection = g_list_append(ct->selection, info);
459         collection_table_selection_add(ct, info, SELECTION_SELECTED, nullptr);
460
461         collection_table_update_status(ct);
462 }
463
464 static void collection_table_unselect(CollectTable *ct, CollectInfo *info)
465 {
466         ct->prev_selection = info;
467
468         if (!info || !INFO_SELECTED(info) ) return;
469
470         ct->selection = g_list_remove(ct->selection, info);
471         collection_table_selection_remove(ct, info, SELECTION_SELECTED, nullptr);
472
473         collection_table_update_status(ct);
474 }
475
476 static void collection_table_select_util(CollectTable *ct, CollectInfo *info, gboolean select)
477 {
478         if (select)
479                 {
480                 collection_table_select(ct, info);
481                 }
482         else
483                 {
484                 collection_table_unselect(ct, info);
485                 }
486 }
487
488 static void collection_table_select_region_util(CollectTable *ct, CollectInfo *start, CollectInfo *end, gboolean select)
489 {
490         gint row1;
491         gint col1;
492         gint row2;
493         gint col2;
494         gint t;
495         gint i;
496         gint j;
497
498         if (!collection_table_find_position(ct, start, &row1, &col1) ||
499             !collection_table_find_position(ct, end, &row2, &col2) ) return;
500
501         ct->prev_selection = end;
502
503         if (!options->collections.rectangular_selection)
504                 {
505                 GList *work;
506                 CollectInfo *info;
507
508                 if (g_list_index(ct->cd->list, start) > g_list_index(ct->cd->list, end))
509                         {
510                         info = start;
511                         start = end;
512                         end = info;
513                         }
514
515                 work = g_list_find(ct->cd->list, start);
516                 while (work)
517                         {
518                         info = static_cast<CollectInfo *>(work->data);
519                         collection_table_select_util(ct, info, select);
520
521                         if (work->data != end)
522                                 work = work->next;
523                         else
524                                 work = nullptr;
525                         }
526                 return;
527                 }
528
529         if (row2 < row1)
530                 {
531                 t = row1;
532                 row1 = row2;
533                 row2 = t;
534                 }
535         if (col2 < col1)
536                 {
537                 t = col1;
538                 col1 = col2;
539                 col2 = t;
540                 }
541
542         DEBUG_1("table: %d x %d to %d x %d", row1, col1, row2, col2);
543
544         for (i = row1; i <= row2; i++)
545                 {
546                 for (j = col1; j <= col2; j++)
547                         {
548                         CollectInfo *info = collection_table_find_data(ct, i, j, nullptr);
549                         if (info) collection_table_select_util(ct, info, select);
550                         }
551                 }
552 }
553
554 GList *collection_table_selection_get_list(CollectTable *ct)
555 {
556         return collection_list_to_filelist(ct->selection);
557 }
558
559 static GList *collection_table_get_list(CollectTable *ct)
560 {
561         return collection_list_to_filelist(ct->cd->list);
562 }
563
564 /*
565  *-------------------------------------------------------------------
566  * tooltip type window
567  *-------------------------------------------------------------------
568  */
569
570 static void tip_show(CollectTable *ct)
571 {
572         GtkWidget *label;
573         gint x;
574         gint y;
575         GdkDisplay *display;
576         GdkSeat *seat;
577         GdkDevice *device;
578
579         if (ct->tip_window) return;
580
581         seat = gdk_display_get_default_seat(gdk_window_get_display(gtk_widget_get_window(ct->listview)));
582         device = gdk_seat_get_pointer(seat);
583         gdk_window_get_device_position(gtk_widget_get_window(ct->listview),
584                                                                 device, &x, &y, nullptr);
585
586         ct->tip_info = collection_table_find_data_by_coord(ct, x, y, nullptr);
587         if (!ct->tip_info) return;
588
589         ct->tip_window = gtk_window_new(GTK_WINDOW_POPUP);
590         gtk_window_set_resizable(GTK_WINDOW(ct->tip_window), FALSE);
591         gtk_container_set_border_width(GTK_CONTAINER(ct->tip_window), 2);
592
593         label = gtk_label_new(ct->show_text ? ct->tip_info->fd->path : ct->tip_info->fd->name);
594
595         g_object_set_data(G_OBJECT(ct->tip_window), "tip_label", label);
596         gq_gtk_container_add(GTK_WIDGET(ct->tip_window), label);
597         gtk_widget_show(label);
598
599         display = gdk_display_get_default();
600         seat = gdk_display_get_default_seat(display);
601         device = gdk_seat_get_pointer(seat);
602         gdk_device_get_position(device, nullptr, &x, &y);
603
604         if (!gtk_widget_get_realized(ct->tip_window)) gtk_widget_realize(ct->tip_window);
605         gq_gtk_window_move(GTK_WINDOW(ct->tip_window), x + 16, y + 16);
606         gtk_widget_show(ct->tip_window);
607 }
608
609 static void tip_hide(CollectTable *ct)
610 {
611         if (ct->tip_window) gq_gtk_widget_destroy(ct->tip_window);
612         ct->tip_window = nullptr;
613 }
614
615 static gboolean tip_schedule_cb(gpointer data)
616 {
617         auto ct = static_cast<CollectTable *>(data);
618
619         if (!ct->tip_delay_id) return FALSE;
620
621         tip_show(ct);
622
623         ct->tip_delay_id = 0;
624         return FALSE;
625 }
626
627 static void tip_schedule(CollectTable *ct)
628 {
629         tip_hide(ct);
630
631         if (ct->tip_delay_id)
632                 {
633                 g_source_remove(ct->tip_delay_id);
634                 ct->tip_delay_id = 0;
635                 }
636
637         ct->tip_delay_id = g_timeout_add(ct->show_text ? COLLECT_TABLE_TIP_DELAY_PATH : COLLECT_TABLE_TIP_DELAY, tip_schedule_cb, ct);
638 }
639
640 static void tip_unschedule(CollectTable *ct)
641 {
642         tip_hide(ct);
643
644         if (ct->tip_delay_id)
645                 {
646                 g_source_remove(ct->tip_delay_id);
647                 ct->tip_delay_id = 0;
648                 }
649 }
650
651 static void tip_update(CollectTable *ct, CollectInfo *info)
652 {
653         GdkDisplay *display = gdk_display_get_default();
654         GdkSeat *seat = gdk_display_get_default_seat(display);
655         GdkDevice *device = gdk_seat_get_pointer(seat);
656
657         tip_schedule(ct);
658
659         if (ct->tip_window)
660                 {
661                 gint x;
662                 gint y;
663                 gdk_device_get_position(device, nullptr, &x, &y);
664
665                 gq_gtk_window_move(GTK_WINDOW(ct->tip_window), x + 16, y + 16);
666
667                 if (info != ct->tip_info)
668                         {
669                         GtkWidget *label;
670
671                         ct->tip_info = info;
672
673                         if (!ct->tip_info)
674                                 {
675                                 return;
676                                 }
677
678                         label = static_cast<GtkWidget *>(g_object_get_data(G_OBJECT(ct->tip_window), "tip_label"));
679                         gtk_label_set_text(GTK_LABEL(label), ct->show_text ? ct->tip_info->fd->path : ct->tip_info->fd->name);
680                         }
681                 }
682 }
683
684 /*
685  *-------------------------------------------------------------------
686  * popup menus
687  *-------------------------------------------------------------------
688  */
689
690 static void collection_table_popup_save_as_cb(GtkWidget *, gpointer data)
691 {
692         auto ct = static_cast<CollectTable *>(data);
693
694         collection_dialog_save_as(ct->cd);
695 }
696
697 static void collection_table_popup_save_cb(GtkWidget *widget, gpointer data)
698 {
699         auto ct = static_cast<CollectTable *>(data);
700
701         if (!ct->cd->path)
702                 {
703                 collection_table_popup_save_as_cb(widget, data);
704                 return;
705                 }
706
707         if (!collection_save(ct->cd, ct->cd->path))
708                 {
709                 log_printf("failed saving to collection path: %s\n", ct->cd->path);
710                 }
711 }
712
713 static GList *collection_table_popup_file_list(CollectTable *ct)
714 {
715         if (!ct->click_info) return nullptr;
716
717         if (INFO_SELECTED(ct->click_info))
718                 {
719                 return collection_table_selection_get_list(ct);
720                 }
721
722         return g_list_append(nullptr, file_data_ref(ct->click_info->fd));
723 }
724
725 static void collection_table_popup_edit_cb(GtkWidget *widget, gpointer data)
726 {
727         CollectTable *ct;
728         auto key = static_cast<const gchar *>(data);
729
730         ct = static_cast<CollectTable *>(submenu_item_get_data(widget));
731
732         if (!ct) return;
733
734         file_util_start_editor_from_filelist(key, collection_table_popup_file_list(ct), nullptr, ct->listview);
735 }
736
737 static void collection_table_popup_copy_cb(GtkWidget *, gpointer data)
738 {
739         auto ct = static_cast<CollectTable *>(data);
740
741         file_util_copy(nullptr, collection_table_popup_file_list(ct), nullptr, ct->listview);
742 }
743
744 static void collection_table_popup_move_cb(GtkWidget *, gpointer data)
745 {
746         auto ct = static_cast<CollectTable *>(data);
747
748         file_util_move(nullptr, collection_table_popup_file_list(ct), nullptr, ct->listview);
749 }
750
751 static void collection_table_popup_rename_cb(GtkWidget *, gpointer data)
752 {
753         auto ct = static_cast<CollectTable *>(data);
754
755         file_util_rename(nullptr, collection_table_popup_file_list(ct), ct->listview);
756 }
757
758 static void collection_table_popup_delete_cb(GtkWidget *, gpointer data)
759 {
760         auto ct = static_cast<CollectTable *>(data);
761
762         options->file_ops.safe_delete_enable = FALSE;
763         file_util_delete(nullptr, collection_table_popup_file_list(ct), ct->listview);
764 }
765
766 static void collection_table_popup_move_to_trash_cb(GtkWidget *, gpointer data)
767 {
768         auto ct = static_cast<CollectTable *>(data);
769
770         options->file_ops.safe_delete_enable = TRUE;
771         file_util_delete(nullptr, collection_table_popup_file_list(ct), ct->listview);
772 }
773
774 static void collection_table_popup_copy_path_cb(GtkWidget *, gpointer data)
775 {
776         auto ct = static_cast<CollectTable *>(data);
777
778         file_util_copy_path_list_to_clipboard(collection_table_popup_file_list(ct), TRUE);
779 }
780
781 static void collection_table_popup_copy_path_unquoted_cb(GtkWidget *, gpointer data)
782 {
783         auto ct = static_cast<CollectTable *>(data);
784
785         file_util_copy_path_list_to_clipboard(collection_table_popup_file_list(ct), FALSE);
786 }
787
788 static void collection_table_popup_sort_cb(GtkWidget *widget, gpointer data)
789 {
790         CollectTable *ct;
791         SortType type;
792
793         ct = static_cast<CollectTable *>(submenu_item_get_data(widget));
794
795         if (!ct) return;
796
797         type = static_cast<SortType>GPOINTER_TO_INT(data);
798
799         collection_set_sort_method(ct->cd, type);
800 }
801
802 static void collection_table_popup_randomize_cb(GtkWidget *widget, gpointer)
803 {
804         CollectTable *ct;
805
806         ct = static_cast<CollectTable *>(submenu_item_get_data(widget));
807
808         if (!ct) return;
809
810         collection_randomize(ct->cd);
811 }
812
813 static void collection_table_popup_view_new_cb(GtkWidget *, gpointer data)
814 {
815         auto ct = static_cast<CollectTable *>(data);
816
817         if (ct->click_info && g_list_find(ct->cd->list, ct->click_info))
818                 {
819                 view_window_new_from_collection(ct->cd, ct->click_info);
820                 }
821 }
822
823 static void collection_table_popup_view_cb(GtkWidget *, gpointer data)
824 {
825         auto ct = static_cast<CollectTable *>(data);
826
827         if (ct->click_info && g_list_find(ct->cd->list, ct->click_info))
828                 {
829                 layout_image_set_collection(nullptr, ct->cd, ct->click_info);
830                 }
831 }
832
833 static void collection_table_popup_selectall_cb(GtkWidget *, gpointer data)
834 {
835         auto ct = static_cast<CollectTable *>(data);
836
837         collection_table_select_all(ct);
838         ct->prev_selection= ct->click_info;
839 }
840
841 static void collection_table_popup_unselectall_cb(GtkWidget *, gpointer data)
842 {
843         auto ct = static_cast<CollectTable *>(data);
844
845         collection_table_unselect_all(ct);
846         ct->prev_selection= ct->click_info;
847 }
848
849 static void collection_table_popup_select_invert_cb(GtkWidget *, gpointer data)
850 {
851         auto ct = static_cast<CollectTable *>(data);
852
853         collection_table_select_invert_all(ct);
854         ct->prev_selection= ct->click_info;
855 }
856
857 static void collection_table_popup_rectangular_selection_cb(GtkWidget *, gpointer)
858 {
859         options->collections.rectangular_selection = !(options->collections.rectangular_selection);
860 }
861
862 static void collection_table_popup_remove_cb(GtkWidget *, gpointer data)
863 {
864         auto ct = static_cast<CollectTable *>(data);
865         GList *list;
866
867         if (!ct->click_info) return;
868
869         if (INFO_SELECTED(ct->click_info))
870                 {
871                 list = g_list_copy(ct->selection);
872                 }
873         else
874                 {
875                 list = g_list_append(nullptr, ct->click_info);
876                 }
877
878         collection_remove_by_info_list(ct->cd, list);
879         collection_table_refresh(ct);
880         g_list_free(list);
881 }
882
883 static void collection_table_popup_add_file_selection_cb(GtkWidget *, gpointer data)
884 {
885         auto ct = static_cast<CollectTable *>(data);
886         GList *list;
887         LayoutWindow *lw = nullptr;
888
889         if (!layout_valid(&lw)) return;
890
891         list = vf_selection_get_list(lw->vf);
892
893         if (list)
894                 {
895                 collection_table_add_filelist(ct, list);
896                 filelist_free(list);
897                 }
898 }
899
900 static void collection_table_popup_add_collection_cb(GtkWidget *, gpointer data)
901 {
902         auto ct = static_cast<CollectTable *>(data);
903
904         collection_dialog_append(ct->cd);
905 }
906
907 static void collection_table_popup_goto_original_cb(GtkWidget *, gpointer data)
908 {
909         auto ct = static_cast<CollectTable *>(data);
910         GList *list;
911         LayoutWindow *lw = nullptr;
912         FileData *fd;
913
914         if (!layout_valid(&lw)) return;
915         list = collection_table_selection_get_list(ct);
916         if (list)
917                 {
918                 fd = static_cast<FileData *>(list->data);
919                 if (fd)
920                         {
921                         layout_set_fd(lw, fd);
922                         }
923                 }
924         g_list_free(list);
925 }
926
927 static void collection_table_popup_find_dupes_cb(GtkWidget *, gpointer data)
928 {
929         auto ct = static_cast<CollectTable *>(data);
930         DupeWindow *dw;
931
932         dw = dupe_window_new();
933         dupe_window_add_collection(dw, ct->cd);
934 }
935
936 static void collection_table_popup_print_cb(GtkWidget *, gpointer data)
937 {
938         auto ct = static_cast<CollectTable *>(data);
939         FileData *fd;
940
941         fd = (ct->click_info) ? ct->click_info->fd : nullptr;
942
943         print_window_new(fd, collection_table_selection_get_list(ct), collection_table_get_list(ct), gtk_widget_get_toplevel(ct->listview));
944 }
945
946 static void collection_table_popup_show_names_cb(GtkWidget *, gpointer data)
947 {
948         auto ct = static_cast<CollectTable *>(data);
949
950         collection_table_toggle_filenames(ct);
951 }
952
953 static void collection_table_popup_show_stars_cb(GtkWidget *, gpointer data)
954 {
955         auto ct = static_cast<CollectTable *>(data);
956
957         collection_table_toggle_stars(ct);
958 }
959
960 static void collection_table_popup_destroy_cb(GtkWidget *, gpointer data)
961 {
962         auto ct = static_cast<CollectTable *>(data);
963
964         collection_table_selection_remove(ct, ct->click_info, SELECTION_PRELIGHT, nullptr);
965         ct->click_info = nullptr;
966         ct->popup = nullptr;
967
968         filelist_free(ct->drop_list);
969         ct->drop_list = nullptr;
970         ct->drop_info = nullptr;
971
972         filelist_free(ct->editmenu_fd_list);
973         ct->editmenu_fd_list = nullptr;
974 }
975
976 static GtkWidget *collection_table_popup_menu(CollectTable *ct, gboolean over_icon)
977 {
978         GtkWidget *menu;
979         GtkWidget *item;
980         GtkWidget *submenu;
981         GtkAccelGroup *accel_group;
982
983         menu = popup_menu_short_lived();
984
985         accel_group = gtk_accel_group_new();
986         gtk_menu_set_accel_group(GTK_MENU(menu), accel_group);
987
988         g_object_set_data(G_OBJECT(menu), "window_keys", collection_window_keys);
989         g_object_set_data(G_OBJECT(menu), "accel_group", accel_group);
990
991         g_signal_connect(G_OBJECT(menu), "destroy",
992                          G_CALLBACK(collection_table_popup_destroy_cb), ct);
993
994         menu_item_add_sensitive(menu, _("_View"), over_icon,
995                         G_CALLBACK(collection_table_popup_view_cb), ct);
996         menu_item_add_icon_sensitive(menu, _("View in _new window"), GQ_ICON_NEW, over_icon,
997                         G_CALLBACK(collection_table_popup_view_new_cb), ct);
998         menu_item_add_icon(menu, _("Go to original"), GQ_ICON_FIND,
999                         G_CALLBACK(collection_table_popup_goto_original_cb), ct);
1000         menu_item_add_divider(menu);
1001         menu_item_add_icon_sensitive(menu, _("Rem_ove"), GQ_ICON_REMOVE, over_icon,
1002                         G_CALLBACK(collection_table_popup_remove_cb), ct);
1003
1004         menu_item_add_icon(menu, _("Append from file selection"), GQ_ICON_ADD,
1005                         G_CALLBACK(collection_table_popup_add_file_selection_cb), ct);
1006         menu_item_add_icon(menu, _("Append from collection..."), GQ_ICON_OPEN,
1007                         G_CALLBACK(collection_table_popup_add_collection_cb), ct);
1008         menu_item_add_divider(menu);
1009
1010         item = menu_item_add(menu, _("_Selection"), nullptr, nullptr);
1011         submenu = gtk_menu_new();
1012         menu_item_add(submenu, _("Select all"),
1013                         G_CALLBACK(collection_table_popup_selectall_cb), ct);
1014         menu_item_add(submenu, _("Select none"),
1015                         G_CALLBACK(collection_table_popup_unselectall_cb), ct);
1016         menu_item_add(submenu, _("Invert selection"),
1017                         G_CALLBACK(collection_table_popup_select_invert_cb), ct);
1018         menu_item_add_check(submenu, _("Rectangular selection"), (options->collections.rectangular_selection != FALSE),
1019                         G_CALLBACK(collection_table_popup_rectangular_selection_cb), ct);
1020         gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), submenu);
1021         menu_item_add_divider(menu);
1022
1023
1024         ct->editmenu_fd_list = collection_table_selection_get_list(ct);
1025         submenu_add_edit(menu, &item,
1026                         G_CALLBACK(collection_table_popup_edit_cb), ct, ct->editmenu_fd_list);
1027         gtk_widget_set_sensitive(item, over_icon);
1028
1029         menu_item_add_divider(menu);
1030         menu_item_add_icon_sensitive(menu, _("_Copy..."), GQ_ICON_COPY, over_icon,
1031                         G_CALLBACK(collection_table_popup_copy_cb), ct);
1032         menu_item_add_sensitive(menu, _("_Move..."), over_icon,
1033                         G_CALLBACK(collection_table_popup_move_cb), ct);
1034         menu_item_add_sensitive(menu, _("_Rename..."), over_icon,
1035                         G_CALLBACK(collection_table_popup_rename_cb), ct);
1036         menu_item_add_sensitive(menu, _("_Copy path"), over_icon,
1037                                 G_CALLBACK(collection_table_popup_copy_path_cb), ct);
1038         menu_item_add_sensitive(menu, _("_Copy path unquoted"), over_icon,
1039                                 G_CALLBACK(collection_table_popup_copy_path_unquoted_cb), ct);
1040
1041         menu_item_add_divider(menu);
1042         menu_item_add_icon_sensitive(menu,
1043                                 options->file_ops.confirm_move_to_trash ? _("Move to Trash...") :
1044                                         _("Move to Trash"), GQ_ICON_DELETE, over_icon,
1045                                 G_CALLBACK(collection_table_popup_move_to_trash_cb), ct);
1046         menu_item_add_icon_sensitive(menu,
1047                                 options->file_ops.confirm_delete ? _("_Delete...") :
1048                                         _("_Delete"), GQ_ICON_DELETE_SHRED, over_icon,
1049                                 G_CALLBACK(collection_table_popup_delete_cb), ct);
1050
1051         menu_item_add_divider(menu);
1052         submenu = submenu_add_sort(nullptr, G_CALLBACK(collection_table_popup_sort_cb), ct, FALSE, TRUE, FALSE, SORT_NONE);
1053         menu_item_add_divider(submenu);
1054         menu_item_add(submenu, _("Randomize"),
1055                         G_CALLBACK(collection_table_popup_randomize_cb), ct);
1056         item = menu_item_add(menu, _("_Sort"), nullptr, nullptr);
1057         gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), submenu);
1058
1059         menu_item_add_check(menu, _("Show filename _text"), ct->show_text,
1060                         G_CALLBACK(collection_table_popup_show_names_cb), ct);
1061         menu_item_add_check(menu, _("Show star rating"), ct->show_stars,
1062                                 G_CALLBACK(collection_table_popup_show_stars_cb), ct);
1063         menu_item_add_divider(menu);
1064         menu_item_add_icon(menu, _("_Save collection"), GQ_ICON_SAVE,
1065                         G_CALLBACK(collection_table_popup_save_cb), ct);
1066         menu_item_add_icon(menu, _("Save collection _as..."), GQ_ICON_SAVE_AS,
1067                         G_CALLBACK(collection_table_popup_save_as_cb), ct);
1068         menu_item_add_divider(menu);
1069         menu_item_add_icon(menu, _("_Find duplicates..."), GQ_ICON_FIND,
1070                         G_CALLBACK(collection_table_popup_find_dupes_cb), ct);
1071         menu_item_add_icon_sensitive(menu, _("Print..."), GQ_ICON_PRINT, over_icon,
1072                         G_CALLBACK(collection_table_popup_print_cb), ct);
1073
1074         return menu;
1075 }
1076 /*
1077  *-------------------------------------------------------------------
1078  * keyboard callbacks
1079  *-------------------------------------------------------------------
1080  */
1081
1082 void collection_table_set_focus(CollectTable *ct, CollectInfo *info)
1083 {
1084         GtkTreeIter iter;
1085         gint row;
1086         gint col;
1087
1088         if (g_list_find(ct->cd->list, ct->focus_info))
1089                 {
1090                 if (info == ct->focus_info)
1091                         {
1092                         /* ensure focus row col are correct */
1093                         collection_table_find_position(ct, ct->focus_info,
1094                                                        &ct->focus_row, &ct->focus_column);
1095                         return;
1096                         }
1097                 collection_table_selection_remove(ct, ct->focus_info, SELECTION_FOCUS, nullptr);
1098                 }
1099
1100         if (!collection_table_find_position(ct, info, &row, &col))
1101                 {
1102                 ct->focus_info = nullptr;
1103                 ct->focus_row = -1;
1104                 ct->focus_column = -1;
1105                 return;
1106                 }
1107
1108         ct->focus_info = info;
1109         ct->focus_row = row;
1110         ct->focus_column = col;
1111         collection_table_selection_add(ct, ct->focus_info, SELECTION_FOCUS, nullptr);
1112
1113         if (collection_table_find_iter(ct, ct->focus_info, &iter, nullptr))
1114                 {
1115                 GtkTreePath *tpath;
1116                 GtkTreeViewColumn *column;
1117                 GtkTreeModel *store;
1118
1119                 tree_view_row_make_visible(GTK_TREE_VIEW(ct->listview), &iter, FALSE);
1120
1121                 store = gtk_tree_view_get_model(GTK_TREE_VIEW(ct->listview));
1122                 tpath = gtk_tree_model_get_path(store, &iter);
1123                 /* focus is set to an extra column with 0 width to hide focus, we draw it ourself */
1124                 column = gtk_tree_view_get_column(GTK_TREE_VIEW(ct->listview), COLLECT_TABLE_MAX_COLUMNS);
1125                 gtk_tree_view_set_cursor(GTK_TREE_VIEW(ct->listview), tpath, column, FALSE);
1126                 gtk_tree_path_free(tpath);
1127                 }
1128 }
1129
1130 static void collection_table_move_focus(CollectTable *ct, gint row, gint col, gboolean relative)
1131 {
1132         gint new_row;
1133         gint new_col;
1134
1135         if (relative)
1136                 {
1137                 new_row = ct->focus_row;
1138                 new_col = ct->focus_column;
1139
1140                 new_row += row;
1141                 if (new_row < 0) new_row = 0;
1142                 if (new_row >= ct->rows) new_row = ct->rows - 1;
1143
1144                 while (col != 0)
1145                         {
1146                         if (col < 0)
1147                                 {
1148                                 new_col--;
1149                                 col++;
1150                                 }
1151                         else
1152                                 {
1153                                 new_col++;
1154                                 col--;
1155                                 }
1156
1157                         if (new_col < 0)
1158                                 {
1159                                 if (new_row > 0)
1160                                         {
1161                                         new_row--;
1162                                         new_col = ct->columns - 1;
1163                                         }
1164                                 else
1165                                         {
1166                                         new_col = 0;
1167                                         }
1168                                 }
1169                         if (new_col >= ct->columns)
1170                                 {
1171                                 if (new_row < ct->rows - 1)
1172                                         {
1173                                         new_row++;
1174                                         new_col = 0;
1175                                         }
1176                                 else
1177                                         {
1178                                         new_col = ct->columns - 1;
1179                                         }
1180                                 }
1181                         }
1182                 }
1183         else
1184                 {
1185                 new_row = row;
1186                 new_col = col;
1187
1188                 if (new_row >= ct->rows)
1189                         {
1190                         if (ct->rows > 0)
1191                                 new_row = ct->rows - 1;
1192                         else
1193                                 new_row = 0;
1194                         new_col = ct->columns - 1;
1195                         }
1196                 if (new_col >= ct->columns) new_col = ct->columns - 1;
1197                 }
1198
1199         if (new_row == ct->rows - 1)
1200                 {
1201                 gint l;
1202
1203                 /* if we moved beyond the last image, go to the last image */
1204
1205                 l = g_list_length(ct->cd->list);
1206                 if (ct->rows > 1) l -= (ct->rows - 1) * ct->columns;
1207                 if (new_col >= l) new_col = l - 1;
1208                 }
1209
1210         if (new_row == -1 || new_col == -1)
1211                 {
1212                 if (!ct->cd->list) return;
1213                 new_row = new_col = 0;
1214                 }
1215
1216         collection_table_set_focus(ct, collection_table_find_data(ct, new_row, new_col, nullptr));
1217 }
1218
1219 static void collection_table_update_focus(CollectTable *ct)
1220 {
1221         gint new_row = 0;
1222         gint new_col = 0;
1223
1224         if (ct->focus_info && collection_table_find_position(ct, ct->focus_info, &new_row, &new_col))
1225                 {
1226                 /* first find the old focus, if it exists and is valid */
1227                 }
1228         else
1229                 {
1230                 /* (try to) stay where we were */
1231                 new_row = ct->focus_row;
1232                 new_col = ct->focus_column;
1233                 }
1234
1235         collection_table_move_focus(ct, new_row, new_col, FALSE);
1236 }
1237
1238 /* used to figure the page up/down distances */
1239 static gint page_height(CollectTable *ct)
1240 {
1241         GtkAdjustment *adj;
1242         gint page_size;
1243         gint row_height;
1244         gint ret;
1245
1246         adj = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(ct->listview));
1247         page_size = static_cast<gint>(gtk_adjustment_get_page_increment(adj));
1248
1249         row_height = options->thumbnails.max_height + THUMB_BORDER_PADDING * 2;
1250         if (ct->show_text) row_height += options->thumbnails.max_height / 3;
1251
1252         ret = page_size / row_height;
1253         if (ret < 1) ret = 1;
1254
1255         return ret;
1256 }
1257
1258 static gboolean collection_table_press_key_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
1259 {
1260         auto ct = static_cast<CollectTable *>(data);
1261         gint focus_row = 0;
1262         gint focus_col = 0;
1263         CollectInfo *info;
1264         gboolean stop_signal = TRUE;
1265
1266         switch (event->keyval)
1267                 {
1268                 case GDK_KEY_Left: case GDK_KEY_KP_Left:
1269                         focus_col = -1;
1270                         break;
1271                 case GDK_KEY_Right: case GDK_KEY_KP_Right:
1272                         focus_col = 1;
1273                         break;
1274                 case GDK_KEY_Up: case GDK_KEY_KP_Up:
1275                         focus_row = -1;
1276                         break;
1277                 case GDK_KEY_Down: case GDK_KEY_KP_Down:
1278                         focus_row = 1;
1279                         break;
1280                 case GDK_KEY_Page_Up: case GDK_KEY_KP_Page_Up:
1281                         focus_row = -page_height(ct);
1282                         break;
1283                 case GDK_KEY_Page_Down: case GDK_KEY_KP_Page_Down:
1284                         focus_row = page_height(ct);
1285                         break;
1286                 case GDK_KEY_Home: case GDK_KEY_KP_Home:
1287                         focus_row = -ct->focus_row;
1288                         focus_col = -ct->focus_column;
1289                         break;
1290                 case GDK_KEY_End: case GDK_KEY_KP_End:
1291                         focus_row = ct->rows - 1 - ct->focus_row;
1292                         focus_col = ct->columns - 1 - ct->focus_column;
1293                         break;
1294                 case GDK_KEY_space:
1295                         info = collection_table_find_data(ct, ct->focus_row, ct->focus_column, nullptr);
1296                         if (info)
1297                                 {
1298                                 ct->click_info = info;
1299                                 if (event->state & GDK_CONTROL_MASK)
1300                                         {
1301                                         collection_table_select_util(ct, info, !INFO_SELECTED(info));
1302                                         }
1303                                 else
1304                                         {
1305                                         collection_table_unselect_all(ct);
1306                                         collection_table_select(ct, info);
1307                                         }
1308                                 }
1309                         break;
1310                 case 'T': case 't':
1311                         if (event->state & GDK_CONTROL_MASK) collection_table_toggle_filenames(ct);
1312                         break;
1313                 case GDK_KEY_Menu:
1314                 case GDK_KEY_F10:
1315                         info = collection_table_find_data(ct, ct->focus_row, ct->focus_column, nullptr);
1316                         ct->click_info = info;
1317
1318                         collection_table_selection_add(ct, ct->click_info, SELECTION_PRELIGHT, nullptr);
1319                         tip_unschedule(ct);
1320
1321                         ct->popup = collection_table_popup_menu(ct, (info != nullptr));
1322                         gtk_menu_popup_at_widget(GTK_MENU(ct->popup), widget, GDK_GRAVITY_SOUTH, GDK_GRAVITY_CENTER, nullptr);
1323
1324                         break;
1325                 default:
1326                         stop_signal = FALSE;
1327                         break;
1328                 }
1329
1330         if (focus_row != 0 || focus_col != 0)
1331                 {
1332                 CollectInfo *new_info;
1333                 CollectInfo *old_info;
1334
1335                 old_info = collection_table_find_data(ct, ct->focus_row, ct->focus_column, nullptr);
1336                 collection_table_move_focus(ct, focus_row, focus_col, TRUE);
1337                 new_info = collection_table_find_data(ct, ct->focus_row, ct->focus_column, nullptr);
1338
1339                 if (new_info != old_info)
1340                         {
1341                         if (event->state & GDK_SHIFT_MASK)
1342                                 {
1343                                 if (!options->collections.rectangular_selection)
1344                                         {
1345                                         collection_table_select_region_util(ct, old_info, new_info, FALSE);
1346                                         }
1347                                 else
1348                                         {
1349                                         collection_table_select_region_util(ct, ct->click_info, old_info, FALSE);
1350                                         }
1351                                 collection_table_select_region_util(ct, ct->click_info, new_info, TRUE);
1352                                 }
1353                         else if (event->state & GDK_CONTROL_MASK)
1354                                 {
1355                                 ct->click_info = new_info;
1356                                 }
1357                         else
1358                                 {
1359                                 ct->click_info = new_info;
1360                                 collection_table_unselect_all(ct);
1361                                 collection_table_select(ct, new_info);
1362                                 }
1363                         }
1364                 }
1365
1366         if (stop_signal)
1367                 {
1368                 tip_unschedule(ct);
1369                 }
1370
1371         return stop_signal;
1372 }
1373
1374 /*
1375  *-------------------------------------------------------------------
1376  * insert marker
1377  *-------------------------------------------------------------------
1378  */
1379
1380 static CollectInfo *collection_table_insert_find(CollectTable *ct, CollectInfo *source, gboolean *after, GdkRectangle *cell,
1381                                                  gboolean use_coord, gint x, gint y)
1382 {
1383         CollectInfo *info = nullptr;
1384         GtkTreeModel *store;
1385         GtkTreeIter iter;
1386         GtkTreePath *tpath;
1387         GtkTreeViewColumn *column;
1388         GdkSeat *seat;
1389         GdkDevice *device;
1390
1391         store = gtk_tree_view_get_model(GTK_TREE_VIEW(ct->listview));
1392
1393         if (!use_coord)
1394                 {
1395                 seat = gdk_display_get_default_seat(gdk_window_get_display(gtk_widget_get_window(ct->listview)));
1396                 device = gdk_seat_get_pointer(seat);
1397                 gdk_window_get_device_position(gtk_widget_get_window(ct->listview),
1398                                                                         device, &x, &y, nullptr);
1399                 }
1400
1401         if (source)
1402                 {
1403                 gint col;
1404                 if (collection_table_find_iter(ct, source, &iter, &col))
1405                         {
1406                         tpath = gtk_tree_model_get_path(store, &iter);
1407                         column = gtk_tree_view_get_column(GTK_TREE_VIEW(ct->listview), col);
1408                         gtk_tree_view_get_background_area(GTK_TREE_VIEW(ct->listview), tpath, column, cell);
1409                         gtk_tree_path_free(tpath);
1410
1411                         info = source;
1412                         *after = !!(x > cell->x + (cell->width / 2));
1413                         }
1414                 return info;
1415                 }
1416
1417         if (gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(ct->listview), x, y,
1418                                           &tpath, &column, nullptr, nullptr))
1419                 {
1420                 GList *list;
1421                 gint n;
1422
1423                 gtk_tree_model_get_iter(store, &iter, tpath);
1424                 gtk_tree_model_get(store, &iter, CTABLE_COLUMN_POINTER, &list, -1);
1425
1426                 n = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(column), "column_number"));
1427                 info = static_cast<CollectInfo *>(g_list_nth_data(list, n));
1428
1429                 if (info)
1430                         {
1431                         gtk_tree_view_get_background_area(GTK_TREE_VIEW(ct->listview), tpath, column, cell);
1432                         *after = !!(x > cell->x + (cell->width / 2));
1433                         }
1434
1435                 gtk_tree_path_free(tpath);
1436                 }
1437
1438         if (info == nullptr)
1439                 {
1440                 GList *work;
1441
1442                 work = g_list_last(ct->cd->list);
1443                 if (work)
1444                         {
1445                         gint col;
1446
1447                         info = static_cast<CollectInfo *>(work->data);
1448                         *after = TRUE;
1449
1450                         if (collection_table_find_iter(ct, info, &iter, &col))
1451                                 {
1452                                 tpath = gtk_tree_model_get_path(store, &iter);
1453                                 column = gtk_tree_view_get_column(GTK_TREE_VIEW(ct->listview), col);
1454                                 gtk_tree_view_get_background_area(GTK_TREE_VIEW(ct->listview), tpath, column, cell);
1455                                 gtk_tree_path_free(tpath);
1456                                 }
1457                         }
1458                 }
1459
1460         return info;
1461 }
1462
1463 static CollectInfo *collection_table_insert_point(CollectTable *ct, gint x, gint y)
1464 {
1465         CollectInfo *info;
1466         GdkRectangle cell;
1467         gboolean after = FALSE;
1468
1469         info = collection_table_insert_find(ct, nullptr, &after, &cell, TRUE, x, y);
1470
1471         if (info && after)
1472                 {
1473                 GList *work;
1474
1475                 work = g_list_find(ct->cd->list, info);
1476                 if (work && work->next)
1477                         {
1478                         info = static_cast<CollectInfo *>(work->next->data);
1479                         }
1480                 else
1481                         {
1482                         info = nullptr;
1483                         }
1484                 }
1485
1486         return info;
1487 }
1488
1489 static void collection_table_insert_marker(CollectTable *ct, CollectInfo *info, gboolean enable)
1490 {
1491         gboolean after = FALSE;
1492         GdkRectangle cell;
1493         GdkWindow *parent;
1494         gint x_parent;
1495         gint y_parent;
1496         GError *error = nullptr;
1497         GInputStream *in_stream;
1498         GdkPixbuf *pb;
1499
1500         parent = gtk_widget_get_window(gtk_widget_get_toplevel(ct->listview));
1501         gdk_window_get_position(parent, &x_parent, &y_parent);
1502
1503         if (!enable)
1504                 {
1505                 if (ct->marker_window) gdk_window_destroy(ct->marker_window);
1506                 ct->marker_window = nullptr;
1507
1508                 return;
1509                 }
1510
1511         info = collection_table_insert_find(ct, info, &after, &cell, FALSE, 0, 0);
1512
1513         /* this setting does not take into account (after), but since it is not really used... */
1514         ct->marker_info = info;
1515
1516         if (!ct->marker_window)
1517                 {
1518                 GdkWindowAttr attributes;
1519                 gint attributes_mask;
1520
1521                 in_stream = g_resources_open_stream(GQ_RESOURCE_PATH_ICONS "/gq-marker.xpm", G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
1522                 pb = gdk_pixbuf_new_from_stream(in_stream, nullptr, &error);
1523                 g_object_unref(in_stream);
1524
1525                 if (error)
1526                         {
1527                         log_printf("warning: collection marker error: %s", error->message);
1528                         g_error_free(error);
1529                         return;
1530                         }
1531
1532                 gint w = gdk_pixbuf_get_width(pb);
1533                 gint h = gdk_pixbuf_get_height(pb);
1534
1535                 attributes.window_type = GDK_WINDOW_CHILD;
1536                 attributes.wclass = GDK_INPUT_OUTPUT;
1537                 attributes.width = w;
1538                 attributes.height = h;
1539                 attributes.event_mask = gtk_widget_get_events(ct->listview);
1540                 attributes_mask = 0;
1541
1542                 ct->marker_window = gdk_window_new(nullptr, &attributes, attributes_mask);
1543
1544                 cairo_region_t *mask;
1545                 cairo_pattern_t *pattern;
1546                 cairo_surface_t *img = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
1547                 cairo_t *cr = cairo_create(img);
1548                 gdk_cairo_set_source_pixbuf(cr, pb, 0, 0);
1549                 cairo_paint(cr);
1550                 /** @FIXME this is a hack to get the background color
1551                  * pattern = cairo_pattern_create_for_surface(img);
1552                  */
1553                 pattern = cairo_pattern_create_rgb (1.0, 0.0, 0.0);
1554                 mask = gdk_cairo_region_create_from_surface(img);
1555                 gdk_window_shape_combine_region(ct->marker_window, mask, 0, 0);
1556                 gdk_window_set_background_pattern(ct->marker_window, pattern);
1557                 cairo_region_destroy(mask);
1558                 cairo_pattern_destroy(pattern);
1559                 cairo_destroy(cr);
1560                 cairo_surface_destroy(img);
1561                 g_object_unref(pb);
1562                 }
1563
1564         if (info)
1565                 {
1566                 gint x;
1567                 gint y;
1568                 gint w;
1569                 gint h;
1570
1571                 w = gdk_window_get_width(ct->marker_window);
1572                 h = gdk_window_get_height(ct->marker_window);
1573
1574                 if (!after)
1575                         {
1576                         x = cell.x;
1577                         }
1578                 else
1579                         {
1580                         x = cell.x + cell.width;
1581                         }
1582                 x -= (w / 2);
1583                 y = cell.y + (cell.height / 2) - (h / 2);
1584
1585                 x = x + x_parent;
1586                 y = y + y_parent;
1587
1588                 gdk_window_move(ct->marker_window, x, y);
1589
1590                 if (!gdk_window_is_visible(ct->marker_window)) gdk_window_show(ct->marker_window);
1591                 }
1592         else
1593                 {
1594                 if (gdk_window_is_visible(ct->marker_window)) gdk_window_hide(ct->marker_window);
1595                 }
1596 }
1597
1598 /*
1599  *-------------------------------------------------------------------
1600  * mouse drag auto-scroll
1601  *-------------------------------------------------------------------
1602  */
1603
1604 static void collection_table_motion_update(CollectTable *ct, gint x, gint y, gboolean drop_event)
1605 {
1606         CollectInfo *info;
1607
1608         info = collection_table_find_data_by_coord(ct, x, y, nullptr);
1609
1610         if (drop_event)
1611                 {
1612                 tip_unschedule(ct);
1613                 collection_table_insert_marker(ct, info, TRUE);
1614                 }
1615         else
1616                 {
1617                 tip_update(ct, info);
1618                 }
1619 }
1620
1621 static gboolean collection_table_auto_scroll_idle_cb(gpointer data)
1622 {
1623         auto ct = static_cast<CollectTable *>(data);
1624         GdkWindow *window;
1625         gint x;
1626         gint y;
1627         gint w;
1628         gint h;
1629         GdkSeat *seat;
1630         GdkDevice *device;
1631
1632         if (!ct->drop_idle_id) return G_SOURCE_REMOVE;
1633
1634         window = gtk_widget_get_window(ct->listview);
1635         seat = gdk_display_get_default_seat(gdk_window_get_display(window));
1636         device = gdk_seat_get_pointer(seat);
1637         gdk_window_get_device_position(window, device, &x, &y, nullptr);
1638
1639         w = gdk_window_get_width(window);
1640         h = gdk_window_get_height(window);
1641         if (x >= 0 && x < w && y >= 0 && y < h)
1642                 {
1643                 collection_table_motion_update(ct, x, y, TRUE);
1644                 }
1645
1646         ct->drop_idle_id = 0;
1647         return G_SOURCE_REMOVE;
1648 }
1649
1650 static gboolean collection_table_auto_scroll_notify_cb(GtkWidget *, gint, gint, gpointer data)
1651 {
1652         auto ct = static_cast<CollectTable *>(data);
1653
1654         if (!ct->drop_idle_id)
1655                 {
1656                 ct->drop_idle_id = g_idle_add(collection_table_auto_scroll_idle_cb, ct);
1657                 }
1658
1659         return TRUE;
1660 }
1661
1662 static void collection_table_scroll(CollectTable *ct, gboolean scroll)
1663 {
1664         if (!scroll)
1665                 {
1666                 if (ct->drop_idle_id)
1667                         {
1668                         g_source_remove(ct->drop_idle_id);
1669                         ct->drop_idle_id = 0;
1670                         }
1671                 widget_auto_scroll_stop(ct->listview);
1672                 collection_table_insert_marker(ct, nullptr, FALSE);
1673                 }
1674         else
1675                 {
1676                 GtkAdjustment *adj = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(ct->listview));
1677                 widget_auto_scroll_start(ct->listview, adj, -1, options->thumbnails.max_height / 2,
1678                                          collection_table_auto_scroll_notify_cb, ct);
1679                 }
1680 }
1681
1682 /*
1683  *-------------------------------------------------------------------
1684  * mouse callbacks
1685  *-------------------------------------------------------------------
1686  */
1687
1688 static gboolean collection_table_motion_cb(GtkWidget *, GdkEventMotion *event, gpointer data)
1689 {
1690         auto ct = static_cast<CollectTable *>(data);
1691
1692         collection_table_motion_update(ct, static_cast<gint>(event->x), static_cast<gint>(event->y), FALSE);
1693
1694         return FALSE;
1695 }
1696
1697 static gboolean collection_table_press_cb(GtkWidget *, GdkEventButton *bevent, gpointer data)
1698 {
1699         auto ct = static_cast<CollectTable *>(data);
1700         GtkTreeIter iter;
1701         CollectInfo *info;
1702
1703         tip_unschedule(ct);
1704
1705         info = collection_table_find_data_by_coord(ct, static_cast<gint>(bevent->x), static_cast<gint>(bevent->y), &iter);
1706
1707         ct->click_info = info;
1708         collection_table_selection_add(ct, ct->click_info, SELECTION_PRELIGHT, &iter);
1709
1710         switch (bevent->button)
1711                 {
1712                 case MOUSE_BUTTON_LEFT:
1713                         if (bevent->type == GDK_2BUTTON_PRESS)
1714                                 {
1715                                 if (info)
1716                                         {
1717                                         layout_image_set_collection(nullptr, ct->cd, info);
1718                                         }
1719                                 }
1720                         else if (!gtk_widget_has_focus(ct->listview))
1721                                 {
1722                                 gtk_widget_grab_focus(ct->listview);
1723                                 }
1724                         break;
1725                 case MOUSE_BUTTON_RIGHT:
1726                         ct->popup = collection_table_popup_menu(ct, (info != nullptr));
1727                         gtk_menu_popup_at_pointer(GTK_MENU(ct->popup), nullptr);
1728                         break;
1729                 default:
1730                         break;
1731                 }
1732
1733         return TRUE;
1734 }
1735
1736 static gboolean collection_table_release_cb(GtkWidget *, GdkEventButton *bevent, gpointer data)
1737 {
1738         auto ct = static_cast<CollectTable *>(data);
1739         GtkTreeIter iter;
1740         CollectInfo *info = nullptr;
1741
1742         tip_schedule(ct);
1743
1744         if (static_cast<gint>(bevent->x) != 0 || static_cast<gint>(bevent->y) != 0)
1745                 {
1746                 info = collection_table_find_data_by_coord(ct, static_cast<gint>(bevent->x), static_cast<gint>(bevent->y), &iter);
1747                 }
1748
1749         if (ct->click_info)
1750                 {
1751                 collection_table_selection_remove(ct, ct->click_info, SELECTION_PRELIGHT, nullptr);
1752                 }
1753
1754         if (bevent->button == MOUSE_BUTTON_LEFT &&
1755             info && ct->click_info == info)
1756                 {
1757                 collection_table_set_focus(ct, info);
1758
1759                 if (bevent->state & GDK_CONTROL_MASK)
1760                         {
1761                         gboolean select = !INFO_SELECTED(info);
1762
1763                         if ((bevent->state & GDK_SHIFT_MASK) && ct->prev_selection)
1764                                 {
1765                                 collection_table_select_region_util(ct, ct->prev_selection, info, select);
1766                                 }
1767                         else
1768                                 {
1769                                 collection_table_select_util(ct, info, select);
1770                                 }
1771                         }
1772                 else
1773                         {
1774                         collection_table_unselect_all(ct);
1775
1776                         if ((bevent->state & GDK_SHIFT_MASK) &&
1777                             ct->prev_selection)
1778                                 {
1779                                 collection_table_select_region_util(ct, ct->prev_selection, info, TRUE);
1780                                 }
1781                         else
1782                                 {
1783                                 collection_table_select_util(ct, info, TRUE);
1784                                 }
1785                         }
1786                 }
1787         else if (bevent->button == MOUSE_BUTTON_MIDDLE &&
1788                  info && ct->click_info == info)
1789                 {
1790                 collection_table_select_util(ct, info, !INFO_SELECTED(info));
1791                 }
1792
1793         return TRUE;
1794 }
1795
1796 static gboolean collection_table_leave_cb(GtkWidget *, GdkEventCrossing *, gpointer data)
1797 {
1798         auto ct = static_cast<CollectTable *>(data);
1799
1800         tip_unschedule(ct);
1801         return FALSE;
1802 }
1803
1804 /*
1805  *-------------------------------------------------------------------
1806  * populate, add, insert, etc.
1807  *-------------------------------------------------------------------
1808  */
1809
1810 static gboolean collection_table_destroy_node_cb(GtkTreeModel *store, GtkTreePath *, GtkTreeIter *iter, gpointer)
1811 {
1812         GList *list;
1813
1814         gtk_tree_model_get(store, iter, CTABLE_COLUMN_POINTER, &list, -1);
1815         g_list_free(list);
1816
1817         return FALSE;
1818 }
1819
1820 static void collection_table_clear_store(CollectTable *ct)
1821 {
1822         GtkTreeModel *store;
1823
1824         store = gtk_tree_view_get_model(GTK_TREE_VIEW(ct->listview));
1825         gtk_tree_model_foreach(store, collection_table_destroy_node_cb, nullptr);
1826
1827         gtk_list_store_clear(GTK_LIST_STORE(store));
1828 }
1829
1830 static GList *collection_table_add_row(CollectTable *ct, GtkTreeIter *iter)
1831 {
1832         GtkListStore *store;
1833         GList *list = nullptr;
1834         gint i;
1835
1836         for (i = 0; i < ct->columns; i++) list = g_list_prepend(list, nullptr);
1837
1838         store = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(ct->listview)));
1839         gtk_list_store_append(store, iter);
1840         gtk_list_store_set(store, iter, CTABLE_COLUMN_POINTER, list, -1);
1841
1842         return list;
1843 }
1844
1845 static void collection_table_populate(CollectTable *ct, gboolean resize)
1846 {
1847         gint row;
1848         GList *work;
1849
1850         collection_table_verify_selections(ct);
1851
1852         collection_table_clear_store(ct);
1853
1854         if (resize)
1855                 {
1856                 gint i;
1857                 gint thumb_width;
1858
1859                 thumb_width = collection_table_get_icon_width(ct);
1860
1861                 for (i = 0; i < COLLECT_TABLE_MAX_COLUMNS; i++)
1862                         {
1863                         GtkTreeViewColumn *column;
1864                         GtkCellRenderer *cell;
1865                         GList *list;
1866
1867                         column = gtk_tree_view_get_column(GTK_TREE_VIEW(ct->listview), i);
1868                         gtk_tree_view_column_set_visible(column, (i < ct->columns));
1869                         gtk_tree_view_column_set_fixed_width(column, thumb_width + (THUMB_BORDER_PADDING * 6));
1870
1871                         list = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(column));
1872                         cell = static_cast<GtkCellRenderer *>((list) ? list->data : nullptr);
1873                         g_list_free(list);
1874
1875                         if (cell && GQV_IS_CELL_RENDERER_ICON(cell))
1876                                 {
1877                                 g_object_set(G_OBJECT(cell), "fixed_width", thumb_width,
1878                                                              "fixed_height", options->thumbnails.max_height,
1879                                                              "show_text", ct->show_text || ct->show_stars, NULL);
1880                                 }
1881                         }
1882                 if (gtk_widget_get_realized(ct->listview)) gtk_tree_view_columns_autosize(GTK_TREE_VIEW(ct->listview));
1883                 }
1884
1885         row = -1;
1886         work = ct->cd->list;
1887         while (work)
1888                 {
1889                 GList *list;
1890                 GtkTreeIter iter;
1891
1892                 row++;
1893
1894                 list = collection_table_add_row(ct, &iter);
1895                 while (work && list)
1896                         {
1897                         list->data = work->data;
1898                         list = list->next;
1899                         work = work->next;
1900                         }
1901                 }
1902
1903         ct->rows = row + 1;
1904
1905         collection_table_update_focus(ct);
1906         collection_table_update_status(ct);
1907 }
1908
1909 static void collection_table_populate_at_new_size(CollectTable *ct, gint w, gint, gboolean force)
1910 {
1911         gint new_cols;
1912         gint thumb_width;
1913
1914         thumb_width = collection_table_get_icon_width(ct);
1915
1916         new_cols = w / (thumb_width + (THUMB_BORDER_PADDING * 6));
1917         if (new_cols < 1) new_cols = 1;
1918
1919         if (!force && new_cols == ct->columns) return;
1920
1921         ct->columns = new_cols;
1922
1923         collection_table_populate(ct, TRUE);
1924
1925         DEBUG_1("col tab pop cols=%d rows=%d", ct->columns, ct->rows);
1926 }
1927
1928 static void collection_table_sync(CollectTable *ct)
1929 {
1930         GtkTreeModel *store;
1931         GtkTreeIter iter;
1932         GList *work;
1933         gint r;
1934         gint c;
1935
1936         store = gtk_tree_view_get_model(GTK_TREE_VIEW(ct->listview));
1937
1938         r = -1;
1939         c = 0;
1940
1941         work = ct->cd->list;
1942         while (work)
1943                 {
1944                 GList *list;
1945                 r++;
1946                 c = 0;
1947                 if (gtk_tree_model_iter_nth_child(store, &iter, nullptr, r))
1948                         {
1949                         gtk_tree_model_get(store, &iter, CTABLE_COLUMN_POINTER, &list, -1);
1950                         gtk_list_store_set(GTK_LIST_STORE(store), &iter, CTABLE_COLUMN_POINTER, list, -1);
1951                         }
1952                 else
1953                         {
1954                         list = collection_table_add_row(ct, &iter);
1955                         }
1956
1957                 while (list)
1958                         {
1959                         CollectInfo *info;
1960                         if (work)
1961                                 {
1962                                 info = static_cast<CollectInfo *>(work->data);
1963                                 work = work->next;
1964                                 c++;
1965                                 }
1966                         else
1967                                 {
1968                                 info = nullptr;
1969                                 }
1970                         if (list)
1971                                 {
1972                                 list->data = info;
1973                                 list = list->next;
1974                                 }
1975                         }
1976                 }
1977
1978         r++;
1979         while (gtk_tree_model_iter_nth_child(store, &iter, nullptr, r))
1980                 {
1981                 GList *list;
1982
1983                 gtk_tree_model_get(store, &iter, CTABLE_COLUMN_POINTER, &list, -1);
1984                 gtk_list_store_remove(GTK_LIST_STORE(store), &iter);
1985                 g_list_free(list);
1986                 }
1987
1988         ct->rows = r;
1989
1990         collection_table_update_focus(ct);
1991         collection_table_update_status(ct);
1992 }
1993
1994 static gboolean collection_table_sync_idle_cb(gpointer data)
1995 {
1996         auto ct = static_cast<CollectTable *>(data);
1997
1998         if (!ct->sync_idle_id) return G_SOURCE_REMOVE;
1999         g_source_remove(ct->sync_idle_id);
2000         ct->sync_idle_id = 0;
2001
2002         collection_table_sync(ct);
2003         return G_SOURCE_REMOVE;
2004 }
2005
2006 static void collection_table_sync_idle(CollectTable *ct)
2007 {
2008         if (!ct->sync_idle_id)
2009                 {
2010                 /* high priority, the view needs to be resynced before a redraw
2011                  * may contain invalid pointers at this time
2012                  */
2013                 ct->sync_idle_id = g_idle_add_full(G_PRIORITY_HIGH, collection_table_sync_idle_cb, ct, nullptr);
2014                 }
2015 }
2016
2017 void collection_table_add_filelist(CollectTable *ct, GList *list)
2018 {
2019         GList *work;
2020
2021         if (!list) return;
2022
2023         work = list;
2024         while (work)
2025                 {
2026                 collection_add(ct->cd, static_cast<FileData *>(work->data), FALSE);
2027                 work = work->next;
2028                 }
2029 }
2030
2031 static void collection_table_insert_filelist(CollectTable *ct, GList *list, CollectInfo *insert_info)
2032 {
2033         GList *work;
2034
2035         if (!list) return;
2036
2037         work = list;
2038         while (work)
2039                 {
2040                 collection_insert(ct->cd, static_cast<FileData *>(work->data), insert_info, FALSE);
2041                 work = work->next;
2042                 }
2043
2044         collection_table_sync_idle(ct);
2045 }
2046
2047 static void collection_table_move_by_info_list(CollectTable *ct, GList *info_list, gint row, gint col)
2048 {
2049         GList *work;
2050         GList *insert_pos = nullptr;
2051         GList *temp;
2052         CollectInfo *info;
2053
2054         if (!info_list) return;
2055
2056         info = collection_table_find_data(ct, row, col, nullptr);
2057
2058         if (!info_list->next && info_list->data == info) return;
2059
2060         if (info) insert_pos = g_list_find(ct->cd->list, info);
2061
2062         /** @FIXME this may get slow for large lists */
2063         work = info_list;
2064         while (insert_pos && work)
2065                 {
2066                 if (insert_pos->data == work->data)
2067                         {
2068                         insert_pos = insert_pos->next;
2069                         work = info_list;
2070                         }
2071                 else
2072                         {
2073                         work = work->next;
2074                         }
2075                 }
2076
2077         work = info_list;
2078         while (work)
2079                 {
2080                 ct->cd->list = g_list_remove(ct->cd->list, work->data);
2081                 work = work->next;
2082                 }
2083
2084         /* place them back in */
2085         temp = g_list_copy(info_list);
2086
2087         if (insert_pos)
2088                 {
2089                 ct->cd->list = uig_list_insert_list(ct->cd->list, insert_pos, temp);
2090                 }
2091         else if (info)
2092                 {
2093                 ct->cd->list = g_list_concat(temp, ct->cd->list);
2094                 }
2095         else
2096                 {
2097                 ct->cd->list = g_list_concat(ct->cd->list, temp);
2098                 }
2099
2100         ct->cd->changed = TRUE;
2101
2102         collection_table_sync_idle(ct);
2103 }
2104
2105
2106 /*
2107  *-------------------------------------------------------------------
2108  * updating
2109  *-------------------------------------------------------------------
2110  */
2111
2112 void collection_table_file_update(CollectTable *ct, CollectInfo *info)
2113 {
2114         GtkTreeIter iter;
2115         gint row;
2116         gint col;
2117         gdouble value;
2118
2119         if (!info)
2120                 {
2121                 collection_table_update_extras(ct, FALSE, 0.0);
2122                 return;
2123                 }
2124
2125         if (!collection_table_find_position(ct, info, &row, &col)) return;
2126
2127         if (ct->columns != 0 && ct->rows != 0)
2128                 {
2129                 value = static_cast<gdouble>(row * ct->columns + col) / (ct->columns * ct->rows);
2130                 }
2131         else
2132                 {
2133                 value = 0.0;
2134                 }
2135
2136         collection_table_update_extras(ct, TRUE, value);
2137
2138         if (collection_table_find_iter(ct, info, &iter, nullptr))
2139                 {
2140                 GtkTreeModel *store;
2141                 GList *list;
2142
2143                 store = gtk_tree_view_get_model(GTK_TREE_VIEW(ct->listview));
2144                 gtk_tree_model_get(store, &iter, CTABLE_COLUMN_POINTER, &list, -1);
2145                 gtk_list_store_set(GTK_LIST_STORE(store), &iter, CTABLE_COLUMN_POINTER, list, -1);
2146                 }
2147 }
2148
2149 void collection_table_file_add(CollectTable *ct, CollectInfo *)
2150 {
2151         collection_table_sync_idle(ct);
2152 }
2153
2154 void collection_table_file_insert(CollectTable *ct, CollectInfo *)
2155 {
2156         collection_table_sync_idle(ct);
2157 }
2158
2159 void collection_table_file_remove(CollectTable *ct, CollectInfo *ci)
2160 {
2161         if (ci && INFO_SELECTED(ci))
2162                 {
2163                 ct->selection = g_list_remove(ct->selection, ci);
2164                 }
2165
2166         collection_table_sync_idle(ct);
2167 }
2168
2169 void collection_table_refresh(CollectTable *ct)
2170 {
2171         collection_table_populate(ct, FALSE);
2172 }
2173
2174 /*
2175  *-------------------------------------------------------------------
2176  * dnd
2177  *-------------------------------------------------------------------
2178  */
2179
2180 static void collection_table_add_dir_recursive(CollectTable *ct, FileData *dir_fd, gboolean recursive)
2181 {
2182         GList *d;
2183         GList *f;
2184         GList *work;
2185
2186         if (!filelist_read(dir_fd, &f, recursive ? &d : nullptr))
2187                 return;
2188
2189         f = filelist_filter(f, FALSE);
2190         d = filelist_filter(d, TRUE);
2191
2192         f = filelist_sort_path(f);
2193         d = filelist_sort_path(d);
2194
2195         collection_table_insert_filelist(ct, f, ct->marker_info);
2196
2197         work = g_list_last(d);
2198         while (work)
2199                 {
2200                 collection_table_add_dir_recursive(ct, static_cast<FileData *>(work->data), TRUE);
2201                 work = work->prev;
2202                 }
2203
2204         filelist_free(f);
2205         filelist_free(d);
2206 }
2207
2208 static void confirm_dir_list_do(CollectTable *ct, GList *list, gboolean recursive)
2209 {
2210         GList *work = list;
2211         while (work)
2212                 {
2213                 auto fd = static_cast<FileData *>(work->data);
2214                 work = work->next;
2215                 if (isdir(fd->path)) collection_table_add_dir_recursive(ct, fd, recursive);
2216                 }
2217         collection_table_insert_filelist(ct, list, ct->marker_info);
2218 }
2219
2220
2221 static void confirm_dir_list_add(GtkWidget *, gpointer data)
2222 {
2223         auto ct = static_cast<CollectTable *>(data);
2224
2225         confirm_dir_list_do(ct, ct->drop_list, FALSE);
2226 }
2227
2228 static void confirm_dir_list_recurse(GtkWidget *, gpointer data)
2229 {
2230         auto ct = static_cast<CollectTable *>(data);
2231
2232         confirm_dir_list_do(ct, ct->drop_list, TRUE);
2233 }
2234
2235 static void confirm_dir_list_skip(GtkWidget *, gpointer data)
2236 {
2237         auto ct = static_cast<CollectTable *>(data);
2238
2239         collection_table_insert_filelist(ct, ct->drop_list, ct->marker_info);
2240 }
2241
2242 static GtkWidget *collection_table_drop_menu(CollectTable *ct)
2243 {
2244         GtkWidget *menu;
2245
2246         menu = popup_menu_short_lived();
2247         g_signal_connect(G_OBJECT(menu), "destroy",
2248                          G_CALLBACK(collection_table_popup_destroy_cb), ct);
2249
2250         menu_item_add_stock(menu, _("Dropped list includes folders."), GQ_ICON_DND, nullptr, nullptr);
2251         menu_item_add_divider(menu);
2252         menu_item_add_icon(menu, _("_Add contents"), GQ_ICON_OK,
2253                             G_CALLBACK(confirm_dir_list_add), ct);
2254         menu_item_add_icon(menu, _("Add contents _recursive"), GQ_ICON_ADD,
2255                             G_CALLBACK(confirm_dir_list_recurse), ct);
2256         menu_item_add_icon(menu, _("_Skip folders"), GQ_ICON_REMOVE,
2257                             G_CALLBACK(confirm_dir_list_skip), ct);
2258         menu_item_add_divider(menu);
2259         menu_item_add_icon(menu, _("Cancel"), GQ_ICON_CANCEL, nullptr, ct);
2260
2261         return menu;
2262 }
2263
2264 /*
2265  *-------------------------------------------------------------------
2266  * dnd
2267  *-------------------------------------------------------------------
2268  */
2269
2270 static GtkTargetEntry collection_drag_types[] = {
2271         { const_cast<gchar *>(TARGET_APP_COLLECTION_MEMBER_STRING), 0, TARGET_APP_COLLECTION_MEMBER },
2272         { const_cast<gchar *>("text/uri-list"), 0, TARGET_URI_LIST },
2273         { const_cast<gchar *>("text/plain"), 0, TARGET_TEXT_PLAIN }
2274 };
2275 static gint n_collection_drag_types = 3;
2276
2277 static GtkTargetEntry collection_drop_types[] = {
2278         { const_cast<gchar *>(TARGET_APP_COLLECTION_MEMBER_STRING), 0, TARGET_APP_COLLECTION_MEMBER },
2279         { const_cast<gchar *>("text/uri-list"), 0, TARGET_URI_LIST }
2280 };
2281 static gint n_collection_drop_types = 2;
2282
2283
2284 static void collection_table_dnd_get(GtkWidget *, GdkDragContext *,
2285                                      GtkSelectionData *selection_data, guint info,
2286                                      guint, gpointer data)
2287 {
2288         auto ct = static_cast<CollectTable *>(data);
2289         gboolean selected;
2290         GList *list = nullptr;
2291         gchar *uri_text = nullptr;
2292         gint total;
2293
2294         if (!ct->click_info) return;
2295
2296         selected = INFO_SELECTED(ct->click_info);
2297
2298         switch (info)
2299                 {
2300                 case TARGET_APP_COLLECTION_MEMBER:
2301                         if (selected)
2302                                 {
2303                                 uri_text = collection_info_list_to_dnd_data(ct->cd, ct->selection, &total);
2304                                 }
2305                         else
2306                                 {
2307                                 list = g_list_append(nullptr, ct->click_info);
2308                                 uri_text = collection_info_list_to_dnd_data(ct->cd, list, &total);
2309                                 g_list_free(list);
2310                                 }
2311                         gtk_selection_data_set(selection_data, gtk_selection_data_get_target(selection_data),
2312                                                 8, reinterpret_cast<guchar *>(uri_text), total);
2313                         g_free(uri_text);
2314                         break;
2315                 case TARGET_URI_LIST:
2316                 case TARGET_TEXT_PLAIN:
2317                 default:
2318                         if (selected)
2319                                 {
2320                                 list = collection_table_selection_get_list(ct);
2321                                 }
2322                         else
2323                                 {
2324                                 list = g_list_append(nullptr, file_data_ref(ct->click_info->fd));
2325                                 }
2326                         if (!list) return;
2327
2328                         uri_selection_data_set_uris_from_filelist(selection_data, list);
2329                         filelist_free(list);
2330                         break;
2331                 }
2332 }
2333
2334
2335 static void collection_table_dnd_receive(GtkWidget *, GdkDragContext *context,
2336                                           gint x, gint y,
2337                                           GtkSelectionData *selection_data, guint info,
2338                                           guint, gpointer data)
2339 {
2340         auto ct = static_cast<CollectTable *>(data);
2341         GList *list = nullptr;
2342         GList *info_list = nullptr;
2343         CollectionData *source;
2344         CollectInfo *drop_info;
2345         GList *work;
2346
2347         DEBUG_1("%s", gtk_selection_data_get_data(selection_data));
2348
2349         collection_table_scroll(ct, FALSE);
2350         collection_table_insert_marker(ct, nullptr, FALSE);
2351
2352         drop_info = collection_table_insert_point(ct, x, y);
2353
2354         switch (info)
2355                 {
2356                 case TARGET_APP_COLLECTION_MEMBER:
2357                         source = collection_from_dnd_data(reinterpret_cast<const gchar *>(gtk_selection_data_get_data(selection_data)), &list, &info_list);
2358                         if (source)
2359                                 {
2360                                 if (source == ct->cd)
2361                                         {
2362                                         gint row = -1;
2363                                         gint col = -1;
2364
2365                                         /* it is a move within a collection */
2366                                         filelist_free(list);
2367                                         list = nullptr;
2368
2369                                         if (!drop_info)
2370                                                 {
2371                                                 collection_table_move_by_info_list(ct, info_list, -1, -1);
2372                                                 }
2373                                         else if (collection_table_find_position(ct, drop_info, &row, &col))
2374                                                 {
2375                                                 collection_table_move_by_info_list(ct, info_list, row, col);
2376                                                 }
2377                                         }
2378                                 else
2379                                         {
2380                                         /* it is a move/copy across collections */
2381                                         if (gdk_drag_context_get_selected_action(context) == GDK_ACTION_MOVE)
2382                                                 {
2383                                                 collection_remove_by_info_list(source, info_list);
2384                                                 }
2385                                         }
2386                                 g_list_free(info_list);
2387                                 }
2388                         break;
2389                 case TARGET_URI_LIST:
2390                         list = uri_filelist_from_gtk_selection_data(selection_data);
2391                         work = list;
2392                         while (work)
2393                                 {
2394                                 auto fd = static_cast<FileData *>(work->data);
2395                                 if (isdir(fd->path))
2396                                         {
2397                                         GtkWidget *menu;
2398
2399                                         ct->drop_list = list;
2400                                         ct->drop_info = drop_info;
2401                                         menu = collection_table_drop_menu(ct);
2402                                         gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr);
2403                                         return;
2404                                         }
2405                                 work = work->next;
2406                                 }
2407                         break;
2408                 default:
2409                         list = nullptr;
2410                         break;
2411                 }
2412
2413         if (list)
2414                 {
2415                 collection_table_insert_filelist(ct, list, drop_info);
2416                 filelist_free(list);
2417                 }
2418 }
2419
2420 static void collection_table_dnd_begin(GtkWidget *widget, GdkDragContext *context, gpointer data)
2421 {
2422         auto ct = static_cast<CollectTable *>(data);
2423
2424         if (ct->click_info && ct->click_info->pixbuf)
2425                 {
2426                 gint items;
2427
2428                 if (INFO_SELECTED(ct->click_info))
2429                         items = g_list_length(ct->selection);
2430                 else
2431                         items = 1;
2432                 dnd_set_drag_icon(widget, context, ct->click_info->pixbuf, items);
2433                 }
2434 }
2435
2436 static void collection_table_dnd_end(GtkWidget *, GdkDragContext *, gpointer data)
2437 {
2438         auto ct = static_cast<CollectTable *>(data);
2439
2440         /* apparently a leave event is not generated on a drop */
2441         tip_unschedule(ct);
2442
2443         collection_table_scroll(ct, FALSE);
2444 }
2445
2446 static gint collection_table_dnd_motion(GtkWidget *, GdkDragContext *, gint x, gint y, guint, gpointer data)
2447 {
2448         auto ct = static_cast<CollectTable *>(data);
2449
2450         collection_table_motion_update(ct, x, y, TRUE);
2451         collection_table_scroll(ct, TRUE);
2452
2453         return FALSE;
2454 }
2455
2456 static void collection_table_dnd_leave(GtkWidget *, GdkDragContext *, guint, gpointer data)
2457 {
2458         auto ct = static_cast<CollectTable *>(data);
2459
2460         collection_table_scroll(ct, FALSE);
2461 }
2462
2463 static void collection_table_dnd_init(CollectTable *ct)
2464 {
2465         gtk_drag_source_set(ct->listview, static_cast<GdkModifierType>(GDK_BUTTON1_MASK | GDK_BUTTON2_MASK),
2466                             collection_drag_types, n_collection_drag_types,
2467                             static_cast<GdkDragAction>(GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK));
2468         g_signal_connect(G_OBJECT(ct->listview), "drag_data_get",
2469                          G_CALLBACK(collection_table_dnd_get), ct);
2470         g_signal_connect(G_OBJECT(ct->listview), "drag_begin",
2471                          G_CALLBACK(collection_table_dnd_begin), ct);
2472         g_signal_connect(G_OBJECT(ct->listview), "drag_end",
2473                          G_CALLBACK(collection_table_dnd_end), ct);
2474
2475         gtk_drag_dest_set(ct->listview,
2476                           static_cast<GtkDestDefaults>(GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_HIGHLIGHT | GTK_DEST_DEFAULT_DROP),
2477                           collection_drop_types, n_collection_drop_types,
2478                           static_cast<GdkDragAction>(GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_ASK));
2479         g_signal_connect(G_OBJECT(ct->listview), "drag_motion",
2480                          G_CALLBACK(collection_table_dnd_motion), ct);
2481         g_signal_connect(G_OBJECT(ct->listview), "drag_leave",
2482                          G_CALLBACK(collection_table_dnd_leave), ct);
2483         g_signal_connect(G_OBJECT(ct->listview), "drag_data_received",
2484                          G_CALLBACK(collection_table_dnd_receive), ct);
2485 }
2486
2487 /*
2488  *-----------------------------------------------------------------------------
2489  * draw, etc.
2490  *-----------------------------------------------------------------------------
2491  */
2492
2493 struct ColumnData
2494 {
2495         CollectTable *ct;
2496         gint number;
2497 };
2498
2499 static void collection_table_cell_data_cb(GtkTreeViewColumn *, GtkCellRenderer *cell,
2500                                           GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data)
2501 {
2502         auto cd = static_cast<ColumnData *>(data);
2503         CollectInfo *info;
2504         CollectTable *ct;
2505         gchar *display_text = nullptr;
2506         gchar *star_rating = nullptr;
2507         GdkRGBA color_bg;
2508         GdkRGBA color_bg_style;
2509         GdkRGBA color_fg;
2510         GdkRGBA color_fg_style;
2511         GList *list;
2512         GtkStyle *style;
2513
2514         ct = cd->ct;
2515
2516         gtk_tree_model_get(tree_model, iter, CTABLE_COLUMN_POINTER, &list, -1);
2517
2518         /** @FIXME this is a primitive hack to stop a crash.
2519          * When compiled with GTK3, if a Collection window containing
2520          * say, 50 or so, images has its width changed, there is a segfault
2521          * https://github.com/BestImageViewer/geeqie/issues/531
2522          */
2523         if (cd->number == COLLECT_TABLE_MAX_COLUMNS) return;
2524
2525         info = static_cast<CollectInfo *>(g_list_nth_data(list, cd->number));
2526
2527         style = gtk_widget_get_style(ct->listview);
2528         if (info && (info->flag_mask & SELECTION_SELECTED) )
2529                 {
2530                 convert_gdkcolor_to_gdkrgba(&style->text[GTK_STATE_SELECTED], &color_fg_style);
2531                 convert_gdkcolor_to_gdkrgba(&style->base[GTK_STATE_SELECTED], &color_bg_style);
2532
2533                 memcpy(&color_fg, &color_fg_style, sizeof(color_fg));
2534                 memcpy(&color_bg, &color_bg_style, sizeof(color_bg));
2535                 }
2536         else
2537                 {
2538                 convert_gdkcolor_to_gdkrgba(&style->text[GTK_STATE_NORMAL], &color_fg_style);
2539                 convert_gdkcolor_to_gdkrgba(&style->base[GTK_STATE_NORMAL], &color_bg_style);
2540
2541                 memcpy(&color_fg, &color_fg_style, sizeof(color_fg));
2542                 memcpy(&color_bg, &color_bg_style, sizeof(color_bg));
2543
2544                 memcpy(&color_fg, &color_fg_style, sizeof(color_fg));
2545                 memcpy(&color_bg, &color_bg_style, sizeof(color_bg));
2546                 }
2547
2548         if (info && (info->flag_mask & SELECTION_PRELIGHT))
2549                 {
2550                 shift_color(&color_bg, -1, 0);
2551                 }
2552
2553         if (ct->show_stars && info && info->fd)
2554                 {
2555                 star_rating = metadata_read_rating_stars(info->fd);
2556                 }
2557         else
2558                 {
2559                 star_rating = g_strdup("");
2560                 }
2561
2562         if (info && info->fd)
2563                 {
2564                 if (ct->show_text && ct->show_stars)
2565                         {
2566                         display_text = g_strconcat(info->fd->name, "\n", star_rating, NULL);
2567                         }
2568                 else if (ct->show_text)
2569                         {
2570                         display_text = g_strdup(info->fd->name);
2571                         }
2572                 else if (ct->show_stars)
2573                         {
2574                         display_text = g_strdup(star_rating);
2575                         }
2576                 else
2577                         {
2578                         display_text = g_strdup("");
2579                         }
2580                 }
2581         else
2582                 {
2583                 display_text = g_strdup("");
2584                 }
2585
2586         if (GQV_IS_CELL_RENDERER_ICON(cell))
2587                 {
2588                 if (info)
2589                         {
2590                         g_object_set(cell,      "pixbuf", info->pixbuf,
2591                                                 "text",  display_text,
2592                                                 "cell-background-rgba", &color_bg,
2593                                                 "cell-background-set", TRUE,
2594                                                 "foreground-rgba", &color_fg,
2595                                                 "foreground-set", TRUE,
2596                                                 "has-focus", (ct->focus_info == info), NULL);
2597                         }
2598                 else
2599                         {
2600                         g_object_set(cell,      "pixbuf", NULL,
2601                                                 "text", NULL,
2602                                                 "cell-background-set", FALSE,
2603                                                 "foreground-set", FALSE,
2604                                                 "has-focus", FALSE,  NULL);
2605                         }
2606                 }
2607
2608         g_free(display_text);
2609         g_free(star_rating);
2610 }
2611
2612 static void collection_table_append_column(CollectTable *ct, gint n)
2613 {
2614         ColumnData *cd;
2615         GtkTreeViewColumn *column;
2616         GtkCellRenderer *renderer;
2617
2618         column = gtk_tree_view_column_new();
2619         gtk_tree_view_column_set_min_width(column, 0);
2620
2621         gtk_tree_view_column_set_sizing(column, GTK_TREE_VIEW_COLUMN_FIXED);
2622         gtk_tree_view_column_set_alignment(column, 0.5);
2623
2624         renderer = gqv_cell_renderer_icon_new();
2625         gtk_tree_view_column_pack_start(column, renderer, FALSE);
2626         g_object_set(G_OBJECT(renderer), "xpad", THUMB_BORDER_PADDING * 2,
2627                                          "ypad", THUMB_BORDER_PADDING,
2628                                          "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE, NULL);
2629
2630         g_object_set_data(G_OBJECT(column), "column_number", GINT_TO_POINTER(n));
2631
2632         cd = g_new0(ColumnData, 1);
2633         cd->ct = ct;
2634         cd->number = n;
2635         gtk_tree_view_column_set_cell_data_func(column, renderer, collection_table_cell_data_cb, cd, g_free);
2636
2637         gtk_tree_view_append_column(GTK_TREE_VIEW(ct->listview), column);
2638 }
2639
2640 /*
2641  *-------------------------------------------------------------------
2642  * init, destruction
2643  *-------------------------------------------------------------------
2644  */
2645
2646 static void collection_table_destroy(GtkWidget *, gpointer data)
2647 {
2648         auto ct = static_cast<CollectTable *>(data);
2649
2650         /* If there is no unsaved data, save the window geometry
2651          */
2652         if (!ct->cd->changed)
2653                 {
2654                 if (!collection_save(ct->cd, ct->cd->path))
2655                         {
2656                         log_printf("failed saving to collection path: %s\n", ct->cd->path);
2657                         }
2658                 }
2659
2660         if (ct->popup)
2661                 {
2662                 g_signal_handlers_disconnect_matched(G_OBJECT(ct->popup), G_SIGNAL_MATCH_DATA,
2663                                                      0, 0, nullptr, nullptr, ct);
2664                 g_object_unref(ct->popup);
2665                 }
2666
2667         if (ct->sync_idle_id) g_source_remove(ct->sync_idle_id);
2668
2669         tip_unschedule(ct);
2670         collection_table_scroll(ct, FALSE);
2671
2672         g_free(ct);
2673 }
2674
2675 static void collection_table_sized(GtkWidget *, GtkAllocation *allocation, gpointer data)
2676 {
2677         auto ct = static_cast<CollectTable *>(data);
2678
2679         collection_table_populate_at_new_size(ct, allocation->width, allocation->height, FALSE);
2680 }
2681
2682 CollectTable *collection_table_new(CollectionData *cd)
2683 {
2684         CollectTable *ct;
2685         GtkListStore *store;
2686         GtkTreeSelection *selection;
2687         gint i;
2688
2689         ct = g_new0(CollectTable, 1);
2690
2691         ct->cd = cd;
2692         ct->show_text = options->show_icon_names;
2693         ct->show_stars = options->show_star_rating;
2694
2695         ct->scrolled = gq_gtk_scrolled_window_new(nullptr, nullptr);
2696         gq_gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(ct->scrolled), GTK_SHADOW_IN);
2697         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(ct->scrolled),
2698                                        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
2699
2700         store = gtk_list_store_new(1, G_TYPE_POINTER);
2701         ct->listview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
2702         g_object_unref(store);
2703
2704         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(ct->listview));
2705         gtk_tree_selection_set_mode(GTK_TREE_SELECTION(selection), GTK_SELECTION_NONE);
2706
2707         gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(ct->listview), FALSE);
2708         gtk_tree_view_set_enable_search(GTK_TREE_VIEW(ct->listview), FALSE);
2709
2710         for (i = 0; i < COLLECT_TABLE_MAX_COLUMNS; i++)
2711                 {
2712                 collection_table_append_column(ct, i);
2713                 }
2714
2715         /* zero width column to hide tree view focus, we draw it ourselves */
2716         collection_table_append_column(ct, i);
2717         /* end column to fill white space */
2718         collection_table_append_column(ct, i);
2719
2720         g_signal_connect(G_OBJECT(ct->listview), "destroy",
2721                          G_CALLBACK(collection_table_destroy), ct);
2722         g_signal_connect(G_OBJECT(ct->listview), "size_allocate",
2723                          G_CALLBACK(collection_table_sized), ct);
2724         g_signal_connect(G_OBJECT(ct->listview), "key_press_event",
2725                          G_CALLBACK(collection_table_press_key_cb), ct);
2726
2727         gq_gtk_container_add(GTK_WIDGET(ct->scrolled), ct->listview);
2728         gtk_widget_show(ct->listview);
2729
2730         collection_table_dnd_init(ct);
2731
2732         gtk_widget_set_events(ct->listview, GDK_POINTER_MOTION_MASK | GDK_BUTTON_RELEASE_MASK |
2733                               static_cast<GdkEventMask>(GDK_BUTTON_PRESS_MASK | GDK_LEAVE_NOTIFY_MASK));
2734         g_signal_connect(G_OBJECT(ct->listview),"button_press_event",
2735                          G_CALLBACK(collection_table_press_cb), ct);
2736         g_signal_connect(G_OBJECT(ct->listview),"button_release_event",
2737                          G_CALLBACK(collection_table_release_cb), ct);
2738         g_signal_connect(G_OBJECT(ct->listview),"motion_notify_event",
2739                          G_CALLBACK(collection_table_motion_cb), ct);
2740         g_signal_connect(G_OBJECT(ct->listview), "leave_notify_event",
2741                          G_CALLBACK(collection_table_leave_cb), ct);
2742
2743         return ct;
2744 }
2745
2746 void collection_table_set_labels(CollectTable *ct, GtkWidget *status, GtkWidget *extra)
2747 {
2748         ct->status_label = status;
2749         ct->extra_label = extra;
2750         collection_table_update_status(ct);
2751         collection_table_update_extras(ct, FALSE, 0.0);
2752 }
2753
2754 CollectInfo *collection_table_get_focus_info(CollectTable *ct)
2755 {
2756         return collection_table_find_data(ct, ct->focus_row, ct->focus_column, nullptr);
2757 }
2758
2759 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */