1#include <gtk/gtk.h>
2
3#define FILE_INFO_TYPE_SELECTION (file_info_selection_get_type ())
4
5G_DECLARE_FINAL_TYPE (FileInfoSelection, file_info_selection, FILE_INFO, SELECTION, GObject)
6
7struct _FileInfoSelection
8{
9 GObject parent_instance;
10
11 GListModel *model;
12};
13
14struct _FileInfoSelectionClass
15{
16 GObjectClass parent_class;
17};
18
19static GType
20file_info_selection_get_item_type (GListModel *list)
21{
22 FileInfoSelection *self = FILE_INFO_SELECTION (ptr: list);
23
24 return g_list_model_get_item_type (list: self->model);
25}
26
27static guint
28file_info_selection_get_n_items (GListModel *list)
29{
30 FileInfoSelection *self = FILE_INFO_SELECTION (ptr: list);
31
32 return g_list_model_get_n_items (list: self->model);
33}
34
35static gpointer
36file_info_selection_get_item (GListModel *list,
37 guint position)
38{
39 FileInfoSelection *self = FILE_INFO_SELECTION (ptr: list);
40
41 return g_list_model_get_item (list: self->model, position);
42}
43
44static void
45file_info_selection_list_model_init (GListModelInterface *iface)
46{
47 iface->get_item_type = file_info_selection_get_item_type;
48 iface->get_n_items = file_info_selection_get_n_items;
49 iface->get_item = file_info_selection_get_item;
50}
51
52static gboolean
53file_info_selection_is_selected (GtkSelectionModel *model,
54 guint position)
55{
56 FileInfoSelection *self = FILE_INFO_SELECTION (ptr: model);
57 gpointer item;
58
59 item = g_list_model_get_item (list: self->model, position);
60 if (item == NULL)
61 return FALSE;
62
63 if (GTK_IS_TREE_LIST_ROW (ptr: item))
64 {
65 GtkTreeListRow *row = item;
66 item = gtk_tree_list_row_get_item (self: row);
67 g_object_unref (object: row);
68 }
69
70 return g_file_info_get_attribute_boolean (info: item, attribute: "filechooser::selected");
71}
72
73static void
74file_info_selection_set_selected (FileInfoSelection *self,
75 guint position,
76 gboolean selected)
77{
78 gpointer item;
79
80 item = g_list_model_get_item (list: self->model, position);
81 if (item == NULL)
82 return;
83
84 if (GTK_IS_TREE_LIST_ROW (ptr: item))
85 {
86 GtkTreeListRow *row = item;
87 item = gtk_tree_list_row_get_item (self: row);
88 g_object_unref (object: row);
89 }
90
91 g_file_info_set_attribute_boolean (info: item, attribute: "filechooser::selected", attr_value: selected);
92}
93
94static gboolean
95file_info_selection_select_item (GtkSelectionModel *model,
96 guint position,
97 gboolean exclusive)
98{
99 FileInfoSelection *self = FILE_INFO_SELECTION (ptr: model);
100
101 if (exclusive)
102 {
103 guint i;
104
105 for (i = 0; i < g_list_model_get_n_items (list: self->model); i++)
106 file_info_selection_set_selected (self, position: i, selected: i == position);
107
108 gtk_selection_model_selection_changed (model, position: 0, n_items: g_list_model_get_n_items (list: self->model));
109 }
110 else
111 {
112 file_info_selection_set_selected (self, position, TRUE);
113
114 gtk_selection_model_selection_changed (model, position, n_items: 1);
115 }
116
117 return TRUE;
118}
119
120static gboolean
121file_info_selection_unselect_item (GtkSelectionModel *model,
122 guint position)
123{
124 FileInfoSelection *self = FILE_INFO_SELECTION (ptr: model);
125
126 file_info_selection_set_selected (self, position, FALSE);
127
128 gtk_selection_model_selection_changed (model, position, n_items: 1);
129
130 return TRUE;
131}
132
133static gboolean
134file_info_selection_select_range (GtkSelectionModel *model,
135 guint position,
136 guint n_items,
137 gboolean exclusive)
138{
139 FileInfoSelection *self = FILE_INFO_SELECTION (ptr: model);
140 guint i;
141
142 if (exclusive)
143 for (i = 0; i < position; i++)
144 file_info_selection_set_selected (self, position: i, FALSE);
145
146 for (i = position; i < position + n_items; i++)
147 file_info_selection_set_selected (self, position: i, TRUE);
148
149 if (exclusive)
150 for (i = position + n_items; i < g_list_model_get_n_items (list: self->model); i++)
151 file_info_selection_set_selected (self, position: i, FALSE);
152
153 if (exclusive)
154 gtk_selection_model_selection_changed (model, position: 0, n_items: g_list_model_get_n_items (list: self->model));
155 else
156 gtk_selection_model_selection_changed (model, position, n_items);
157
158 return TRUE;
159}
160
161static gboolean
162file_info_selection_unselect_range (GtkSelectionModel *model,
163 guint position,
164 guint n_items)
165{
166 FileInfoSelection *self = FILE_INFO_SELECTION (ptr: model);
167 guint i;
168
169 for (i = position; i < position + n_items; i++)
170 file_info_selection_set_selected (self, position: i, FALSE);
171
172 gtk_selection_model_selection_changed (model, position, n_items);
173
174 return TRUE;
175}
176
177static void
178file_info_selection_selection_model_init (GtkSelectionModelInterface *iface)
179{
180 iface->is_selected = file_info_selection_is_selected;
181 iface->select_item = file_info_selection_select_item;
182 iface->unselect_item = file_info_selection_unselect_item;
183 iface->select_range = file_info_selection_select_range;
184 iface->unselect_range = file_info_selection_unselect_range;
185}
186
187G_DEFINE_TYPE_EXTENDED (FileInfoSelection, file_info_selection, G_TYPE_OBJECT, 0,
188 G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL,
189 file_info_selection_list_model_init)
190 G_IMPLEMENT_INTERFACE (GTK_TYPE_SELECTION_MODEL,
191 file_info_selection_selection_model_init))
192
193static void
194file_info_selection_items_changed_cb (GListModel *model,
195 guint position,
196 guint removed,
197 guint added,
198 FileInfoSelection *self)
199{
200 g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position, removed, added);
201}
202
203static void
204file_info_selection_clear_model (FileInfoSelection *self)
205{
206 if (self->model == NULL)
207 return;
208
209 g_signal_handlers_disconnect_by_func (self->model,
210 file_info_selection_items_changed_cb,
211 self);
212 g_clear_object (&self->model);
213}
214
215static void
216file_info_selection_dispose (GObject *object)
217{
218 FileInfoSelection *self = FILE_INFO_SELECTION (ptr: object);
219
220 file_info_selection_clear_model (self);
221
222 G_OBJECT_CLASS (file_info_selection_parent_class)->dispose (object);
223}
224
225static void
226file_info_selection_class_init (FileInfoSelectionClass *klass)
227{
228 GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
229
230 gobject_class->dispose = file_info_selection_dispose;
231}
232
233static void
234file_info_selection_init (FileInfoSelection *self)
235{
236}
237
238static FileInfoSelection *
239file_info_selection_new (GListModel *model)
240{
241 FileInfoSelection *result;
242
243 result = g_object_new (FILE_INFO_TYPE_SELECTION, NULL);
244
245 result->model = g_object_ref (model);
246 g_signal_connect (result->model, "items-changed",
247 G_CALLBACK (file_info_selection_items_changed_cb), result);
248
249 return result;
250}
251
252/*** ---------------------- ***/
253
254GSList *pending = NULL;
255guint active = 0;
256
257static void
258loading_cb (GtkDirectoryList *dir,
259 GParamSpec *pspec,
260 gpointer unused)
261{
262 if (gtk_directory_list_is_loading (self: dir))
263 {
264 active++;
265 /* HACK: ensure loading finishes and the dir doesn't get destroyed */
266 g_object_ref (dir);
267 }
268 else
269 {
270 active--;
271 g_object_unref (object: dir);
272
273 while (active < 20 && pending)
274 {
275 GtkDirectoryList *dir2 = pending->data;
276 pending = g_slist_remove (list: pending, data: dir2);
277 gtk_directory_list_set_file (self: dir2, file: g_object_get_data (G_OBJECT (dir2), key: "file"));
278 g_object_unref (object: dir2);
279 }
280 }
281}
282
283static GtkDirectoryList *
284create_directory_list (GFile *file)
285{
286 GtkDirectoryList *dir;
287
288 dir = gtk_directory_list_new (G_FILE_ATTRIBUTE_STANDARD_TYPE
289 "," G_FILE_ATTRIBUTE_STANDARD_NAME
290 "," G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
291 NULL);
292 gtk_directory_list_set_io_priority (self: dir, G_PRIORITY_DEFAULT_IDLE);
293 g_signal_connect (dir, "notify::loading", G_CALLBACK (loading_cb), NULL);
294 g_assert (!gtk_directory_list_is_loading (dir));
295
296 if (active > 20)
297 {
298 g_object_set_data_full (G_OBJECT (dir), key: "file", g_object_ref (file), destroy: g_object_unref);
299 pending = g_slist_prepend (list: pending, g_object_ref (dir));
300 }
301 else
302 {
303 gtk_directory_list_set_file (self: dir, file);
304 }
305
306 return dir;
307}
308
309static char *
310get_file_path (GFileInfo *info)
311{
312 GFile *file;
313
314 file = G_FILE (g_file_info_get_attribute_object (info, "standard::file"));
315 return g_file_get_path (file);
316}
317
318static GListModel *
319create_list_model_for_directory (gpointer file)
320{
321 GtkDirectoryList *dir;
322 GtkSorter *sorter;
323
324 if (g_file_query_file_type (file, flags: G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL) != G_FILE_TYPE_DIRECTORY)
325 return NULL;
326
327 dir = create_directory_list (file);
328 sorter = GTK_SORTER (ptr: gtk_string_sorter_new (expression: gtk_cclosure_expression_new (G_TYPE_STRING, NULL, n_params: 0, NULL, callback_func: (GCallback) get_file_path, NULL, NULL)));
329
330 return G_LIST_MODEL (ptr: gtk_sort_list_model_new (model: G_LIST_MODEL (ptr: dir), sorter));
331}
332
333typedef struct _RowData RowData;
334struct _RowData
335{
336 GtkWidget *expander;
337 GtkWidget *icon;
338 GtkWidget *name;
339 GCancellable *cancellable;
340
341 GtkTreeListRow *current_item;
342};
343
344static void row_data_notify_item (GtkListItem *item,
345 GParamSpec *pspec,
346 RowData *data);
347static void
348row_data_unbind (RowData *data)
349{
350 if (data->current_item == NULL)
351 return;
352
353 if (data->cancellable)
354 {
355 g_cancellable_cancel (cancellable: data->cancellable);
356 g_clear_object (&data->cancellable);
357 }
358
359 g_clear_object (&data->current_item);
360}
361
362static void
363row_data_update_info (RowData *data,
364 GFileInfo *info)
365{
366 GIcon *icon;
367 const char *thumbnail_path;
368
369 thumbnail_path = g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_THUMBNAIL_PATH);
370 if (thumbnail_path)
371 {
372 /* XXX: not async */
373 GFile *thumbnail_file = g_file_new_for_path (path: thumbnail_path);
374 icon = g_file_icon_new (file: thumbnail_file);
375 g_object_unref (object: thumbnail_file);
376 }
377 else
378 {
379 icon = g_file_info_get_icon (info);
380 }
381
382 gtk_widget_set_visible (widget: data->icon, visible: icon != NULL);
383 gtk_image_set_from_gicon (GTK_IMAGE (data->icon), icon);
384}
385
386static void
387copy_attribute (GFileInfo *to,
388 GFileInfo *from,
389 const char *attribute)
390{
391 GFileAttributeType type;
392 gpointer value;
393
394 if (g_file_info_get_attribute_data (info: from, attribute, type: &type, value_pp: &value, NULL))
395 g_file_info_set_attribute (info: to, attribute, type, value_p: value);
396}
397
398static void
399row_data_got_thumbnail_info_cb (GObject *source,
400 GAsyncResult *res,
401 gpointer _data)
402{
403 RowData *data = _data; /* invalid if operation was cancelled */
404 GFile *file = G_FILE (source);
405 GFileInfo *queried, *info;
406
407 queried = g_file_query_info_finish (file, res, NULL);
408 if (queried == NULL)
409 return;
410
411 /* now we know row is valid */
412
413 info = gtk_tree_list_row_get_item (self: data->current_item);
414
415 copy_attribute (to: info, from: queried, G_FILE_ATTRIBUTE_THUMBNAIL_PATH);
416 copy_attribute (to: info, from: queried, G_FILE_ATTRIBUTE_THUMBNAILING_FAILED);
417 copy_attribute (to: info, from: queried, G_FILE_ATTRIBUTE_STANDARD_ICON);
418
419 g_object_unref (object: queried);
420
421 row_data_update_info (data, info);
422
423 g_clear_object (&data->cancellable);
424}
425
426static void
427row_data_bind (RowData *data,
428 GtkTreeListRow *item)
429{
430 GFileInfo *info;
431
432 row_data_unbind (data);
433
434 if (item == NULL)
435 return;
436
437 data->current_item = g_object_ref (item);
438
439 gtk_tree_expander_set_list_row (self: GTK_TREE_EXPANDER (ptr: data->expander), list_row: item);
440
441 info = gtk_tree_list_row_get_item (self: item);
442
443 if (!g_file_info_has_attribute (info, attribute: "filechooser::queried"))
444 {
445 data->cancellable = g_cancellable_new ();
446 g_file_info_set_attribute_boolean (info, attribute: "filechooser::queried", TRUE);
447 g_file_query_info_async (G_FILE (g_file_info_get_attribute_object (info, "standard::file")),
448 G_FILE_ATTRIBUTE_THUMBNAIL_PATH ","
449 G_FILE_ATTRIBUTE_THUMBNAILING_FAILED ","
450 G_FILE_ATTRIBUTE_STANDARD_ICON,
451 flags: G_FILE_QUERY_INFO_NONE,
452 G_PRIORITY_DEFAULT,
453 cancellable: data->cancellable,
454 callback: row_data_got_thumbnail_info_cb,
455 user_data: data);
456 }
457
458 row_data_update_info (data, info);
459
460 gtk_label_set_label (GTK_LABEL (data->name), str: g_file_info_get_display_name (info));
461
462 g_object_unref (object: info);
463}
464
465static void
466row_data_notify_item (GtkListItem *item,
467 GParamSpec *pspec,
468 RowData *data)
469{
470 row_data_bind (data, item: gtk_list_item_get_item (self: item));
471}
472
473static void
474row_data_free (gpointer _data)
475{
476 RowData *data = _data;
477
478 row_data_unbind (data);
479
480 g_slice_free (RowData, data);
481}
482
483static void
484setup_widget (GtkSignalListItemFactory *factory,
485 GtkListItem *list_item)
486{
487 GtkWidget *box, *child;
488 RowData *data;
489
490 data = g_slice_new0 (RowData);
491
492 box = gtk_box_new (orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 4);
493 gtk_list_item_set_child (self: list_item, child: box);
494
495 child = gtk_label_new (NULL);
496 gtk_label_set_width_chars (GTK_LABEL (child), n_chars: 5);
497 gtk_box_append (GTK_BOX (box), child);
498
499 data->expander = gtk_tree_expander_new ();
500 gtk_box_append (GTK_BOX (box), child: data->expander);
501
502 box = gtk_box_new (orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 4);
503 gtk_tree_expander_set_child (self: GTK_TREE_EXPANDER (ptr: data->expander), child: box);
504
505 data->icon = gtk_image_new ();
506 gtk_box_append (GTK_BOX (box), child: data->icon);
507
508 data->name = gtk_label_new (NULL);
509 gtk_label_set_max_width_chars (GTK_LABEL (data->name), n_chars: 25);
510 gtk_label_set_ellipsize (GTK_LABEL (data->name), mode: PANGO_ELLIPSIZE_END);
511 gtk_box_append (GTK_BOX (box), child: data->name);
512
513 g_signal_connect (list_item, "notify::item", G_CALLBACK (row_data_notify_item), data);
514 g_object_set_data_full (G_OBJECT (list_item), key: "row-data", data, destroy: row_data_free);
515}
516
517static GListModel *
518create_list_model_for_file_info (gpointer file_info,
519 gpointer unused)
520{
521 GFile *file = G_FILE (g_file_info_get_attribute_object (file_info, "standard::file"));
522
523 if (file == NULL)
524 return NULL;
525
526 return create_list_model_for_directory (file);
527}
528
529static gboolean
530update_statusbar (GtkStatusbar *statusbar)
531{
532 GListModel *model = g_object_get_data (G_OBJECT (statusbar), key: "model");
533 GString *string = g_string_new (NULL);
534 guint n;
535 gboolean result = G_SOURCE_REMOVE;
536
537 gtk_statusbar_remove_all (statusbar, context_id: 0);
538
539 n = g_list_model_get_n_items (list: model);
540 g_string_append_printf (string, format: "%u", n);
541 if (GTK_IS_FILTER_LIST_MODEL (ptr: model))
542 {
543 guint n_unfiltered = g_list_model_get_n_items (list: gtk_filter_list_model_get_model (self: GTK_FILTER_LIST_MODEL (ptr: model)));
544 if (n != n_unfiltered)
545 g_string_append_printf (string, format: "/%u", n_unfiltered);
546 }
547 g_string_append (string, val: " items");
548
549 if (pending || active)
550 {
551 g_string_append_printf (string, format: " (%u directories remaining)", active + g_slist_length (list: pending));
552 result = G_SOURCE_CONTINUE;
553 }
554 result = G_SOURCE_CONTINUE;
555
556 gtk_statusbar_push (statusbar, context_id: 0, text: string->str);
557 g_free (mem: string->str);
558
559 return result;
560}
561
562static gboolean
563match_file (gpointer item, gpointer data)
564{
565 GtkWidget *search_entry = data;
566 GFileInfo *info = gtk_tree_list_row_get_item (self: item);
567 GFile *file = G_FILE (g_file_info_get_attribute_object (info, "standard::file"));
568 char *path;
569 gboolean result;
570
571 path = g_file_get_path (file);
572
573 result = strstr (haystack: path, needle: gtk_editable_get_text (GTK_EDITABLE (search_entry))) != NULL;
574
575 g_object_unref (object: info);
576 g_free (mem: path);
577
578 return result;
579}
580
581static void
582search_changed_cb (GtkSearchEntry *entry,
583 GtkFilter *custom_filter)
584{
585 gtk_filter_changed (self: custom_filter, change: GTK_FILTER_CHANGE_DIFFERENT);
586}
587
588int
589main (int argc, char *argv[])
590{
591 GtkWidget *win, *vbox, *sw, *listview, *search_entry, *statusbar;
592 GtkTreeListModel *tree;
593 GtkFilterListModel *filter;
594 GtkFilter *custom_filter;
595 FileInfoSelection *selectionmodel;
596 GFile *root;
597 GListModel *toplevels;
598 GtkListItemFactory *factory;
599
600 gtk_init ();
601
602 win = gtk_window_new ();
603 gtk_window_set_default_size (GTK_WINDOW (win), width: 400, height: 600);
604
605 vbox = gtk_box_new (orientation: GTK_ORIENTATION_VERTICAL, spacing: 0);
606 gtk_window_set_child (GTK_WINDOW (win), child: vbox);
607
608 search_entry = gtk_search_entry_new ();
609 gtk_box_append (GTK_BOX (vbox), child: search_entry);
610
611 sw = gtk_scrolled_window_new ();
612 gtk_widget_set_vexpand (widget: sw, TRUE);
613 gtk_search_entry_set_key_capture_widget (GTK_SEARCH_ENTRY (search_entry), widget: sw);
614 gtk_box_append (GTK_BOX (vbox), child: sw);
615
616 factory = gtk_signal_list_item_factory_new ();
617 g_signal_connect (factory, "setup", G_CALLBACK (setup_widget), NULL);
618 listview = gtk_list_view_new (NULL, factory);
619 gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), child: listview);
620
621 if (argc > 1)
622 root = g_file_new_for_commandline_arg (arg: argv[1]);
623 else
624 root = g_file_new_for_path (path: g_get_current_dir ());
625 tree = gtk_tree_list_model_new (root: create_list_model_for_directory (file: root),
626 FALSE,
627 TRUE,
628 create_func: create_list_model_for_file_info,
629 NULL, NULL);
630 g_object_unref (object: root);
631
632 custom_filter = GTK_FILTER (ptr: gtk_custom_filter_new (match_func: match_file, user_data: search_entry, NULL));
633 filter = gtk_filter_list_model_new (model: G_LIST_MODEL (ptr: tree), filter: custom_filter);
634 g_signal_connect (search_entry, "search-changed", G_CALLBACK (search_changed_cb), custom_filter);
635
636 selectionmodel = file_info_selection_new (model: G_LIST_MODEL (ptr: filter));
637 g_object_unref (object: filter);
638
639 gtk_list_view_set_model (GTK_LIST_VIEW (listview), model: GTK_SELECTION_MODEL (ptr: selectionmodel));
640
641 statusbar = gtk_statusbar_new ();
642 gtk_widget_add_tick_callback (widget: statusbar, callback: (GtkTickCallback) update_statusbar, NULL, NULL);
643 g_object_set_data (G_OBJECT (statusbar), key: "model", data: filter);
644 g_signal_connect_swapped (filter, "items-changed", G_CALLBACK (update_statusbar), statusbar);
645 update_statusbar (GTK_STATUSBAR (statusbar));
646 gtk_box_append (GTK_BOX (vbox), child: statusbar);
647
648 g_object_unref (object: selectionmodel);
649
650 gtk_widget_show (widget: win);
651
652 toplevels = gtk_window_get_toplevels ();
653 while (g_list_model_get_n_items (list: toplevels))
654 g_main_context_iteration (NULL, TRUE);
655
656 return 0;
657}
658

source code of gtk/tests/testlistview.c