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