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