1 | /* |
2 | * Copyright © 2019 Benjamin Otte |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2.1 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | * |
17 | * Authors: Benjamin Otte <otte@gnome.org> |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include "gtkdirectorylist.h" |
23 | |
24 | #include "gtkintl.h" |
25 | #include "gtkprivate.h" |
26 | |
27 | /** |
28 | * GtkDirectoryList: |
29 | * |
30 | * `GtkDirectoryList` is a list model that wraps g_file_enumerate_children_async(). |
31 | * |
32 | * It presents a `GListModel` and fills it asynchronously with the `GFileInfo`s |
33 | * returned from that function. |
34 | * |
35 | * Enumeration will start automatically when a the |
36 | * [property@Gtk.DirectoryList:file] property is set. |
37 | * |
38 | * While the `GtkDirectoryList` is being filled, the |
39 | * [property@Gtk.DirectoryList:loading] property will be set to %TRUE. You can |
40 | * listen to that property if you want to show information like a `GtkSpinner` |
41 | * or a "Loading..." text. |
42 | * |
43 | * If loading fails at any point, the [property@Gtk.DirectoryList:error] |
44 | * property will be set to give more indication about the failure. |
45 | * |
46 | * The `GFileInfo`s returned from a `GtkDirectoryList` have the "standard::file" |
47 | * attribute set to the `GFile` they refer to. This way you can get at the file |
48 | * that is referred to in the same way you would via g_file_enumerator_get_child(). |
49 | * This means you do not need access to the `GtkDirectoryList`, but can access |
50 | * the `GFile` directly from the `GFileInfo` when operating with a `GtkListView` |
51 | * or similar. |
52 | */ |
53 | |
54 | /* random number that everyone else seems to use, too */ |
55 | #define FILES_PER_QUERY 100 |
56 | |
57 | enum { |
58 | PROP_0, |
59 | PROP_ATTRIBUTES, |
60 | PROP_ERROR, |
61 | PROP_FILE, |
62 | PROP_IO_PRIORITY, |
63 | PROP_LOADING, |
64 | PROP_MONITORED, |
65 | NUM_PROPERTIES |
66 | }; |
67 | |
68 | typedef struct _QueuedEvent QueuedEvent; |
69 | struct _QueuedEvent |
70 | { |
71 | GtkDirectoryList *list; |
72 | GFile *file; |
73 | GFileInfo *info; |
74 | GFileMonitorEvent event; |
75 | }; |
76 | |
77 | static void |
78 | free_queued_event (gpointer data) |
79 | { |
80 | QueuedEvent *event = data; |
81 | |
82 | g_clear_object (&event->file); |
83 | g_clear_object (&event->info); |
84 | g_free (mem: event); |
85 | } |
86 | |
87 | struct _GtkDirectoryList |
88 | { |
89 | GObject parent_instance; |
90 | |
91 | char *attributes; |
92 | GFile *file; |
93 | GFileMonitor *monitor; |
94 | gboolean monitored; |
95 | int io_priority; |
96 | |
97 | GCancellable *cancellable; |
98 | GError *error; /* Error while loading */ |
99 | GSequence *items; /* Use GPtrArray or GListStore here? */ |
100 | GQueue events; |
101 | }; |
102 | |
103 | struct _GtkDirectoryListClass |
104 | { |
105 | GObjectClass parent_class; |
106 | }; |
107 | |
108 | static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; |
109 | |
110 | static GType |
111 | gtk_directory_list_get_item_type (GListModel *list) |
112 | { |
113 | return G_TYPE_FILE_INFO; |
114 | } |
115 | |
116 | static guint |
117 | gtk_directory_list_get_n_items (GListModel *list) |
118 | { |
119 | GtkDirectoryList *self = GTK_DIRECTORY_LIST (ptr: list); |
120 | |
121 | return g_sequence_get_length (seq: self->items); |
122 | } |
123 | |
124 | static gpointer |
125 | gtk_directory_list_get_item (GListModel *list, |
126 | guint position) |
127 | { |
128 | GtkDirectoryList *self = GTK_DIRECTORY_LIST (ptr: list); |
129 | GSequenceIter *iter; |
130 | |
131 | iter = g_sequence_get_iter_at_pos (seq: self->items, pos: position); |
132 | |
133 | if (g_sequence_iter_is_end (iter)) |
134 | return NULL; |
135 | else |
136 | return g_object_ref (g_sequence_get (iter)); |
137 | } |
138 | |
139 | static void |
140 | gtk_directory_list_model_init (GListModelInterface *iface) |
141 | { |
142 | iface->get_item_type = gtk_directory_list_get_item_type; |
143 | iface->get_n_items = gtk_directory_list_get_n_items; |
144 | iface->get_item = gtk_directory_list_get_item; |
145 | } |
146 | |
147 | G_DEFINE_TYPE_WITH_CODE (GtkDirectoryList, gtk_directory_list, G_TYPE_OBJECT, |
148 | G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_directory_list_model_init)) |
149 | |
150 | static void |
151 | gtk_directory_list_set_property (GObject *object, |
152 | guint prop_id, |
153 | const GValue *value, |
154 | GParamSpec *pspec) |
155 | { |
156 | GtkDirectoryList *self = GTK_DIRECTORY_LIST (ptr: object); |
157 | |
158 | switch (prop_id) |
159 | { |
160 | case PROP_ATTRIBUTES: |
161 | gtk_directory_list_set_attributes (self, attributes: g_value_get_string (value)); |
162 | break; |
163 | case PROP_FILE: |
164 | gtk_directory_list_set_file (self, file: g_value_get_object (value)); |
165 | break; |
166 | |
167 | case PROP_IO_PRIORITY: |
168 | gtk_directory_list_set_io_priority (self, io_priority: g_value_get_int (value)); |
169 | break; |
170 | |
171 | case PROP_MONITORED: |
172 | gtk_directory_list_set_monitored (self, monitored: g_value_get_boolean (value)); |
173 | break; |
174 | |
175 | default: |
176 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
177 | break; |
178 | } |
179 | } |
180 | |
181 | static void |
182 | gtk_directory_list_get_property (GObject *object, |
183 | guint prop_id, |
184 | GValue *value, |
185 | GParamSpec *pspec) |
186 | { |
187 | GtkDirectoryList *self = GTK_DIRECTORY_LIST (ptr: object); |
188 | |
189 | switch (prop_id) |
190 | { |
191 | case PROP_ATTRIBUTES: |
192 | g_value_set_string (value, v_string: self->attributes); |
193 | break; |
194 | |
195 | case PROP_ERROR: |
196 | g_value_set_boxed (value, v_boxed: self->error); |
197 | break; |
198 | |
199 | case PROP_FILE: |
200 | g_value_set_object (value, v_object: self->file); |
201 | break; |
202 | |
203 | case PROP_IO_PRIORITY: |
204 | g_value_set_int (value, v_int: self->io_priority); |
205 | break; |
206 | |
207 | case PROP_LOADING: |
208 | g_value_set_boolean (value, v_boolean: gtk_directory_list_is_loading (self)); |
209 | break; |
210 | |
211 | case PROP_MONITORED: |
212 | g_value_set_boolean (value, v_boolean: gtk_directory_list_get_monitored (self)); |
213 | break; |
214 | |
215 | default: |
216 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
217 | break; |
218 | } |
219 | } |
220 | |
221 | static gboolean |
222 | gtk_directory_list_stop_loading (GtkDirectoryList *self) |
223 | { |
224 | if (self->cancellable == NULL) |
225 | return FALSE; |
226 | |
227 | g_cancellable_cancel (cancellable: self->cancellable); |
228 | g_clear_object (&self->cancellable); |
229 | return TRUE; |
230 | } |
231 | |
232 | static void directory_changed (GFileMonitor *monitor, |
233 | GFile *file, |
234 | GFile *other_file, |
235 | GFileMonitorEvent event, |
236 | gpointer data); |
237 | |
238 | static void |
239 | gtk_directory_list_stop_monitoring (GtkDirectoryList *self) |
240 | { |
241 | if (self->monitor) |
242 | g_signal_handlers_disconnect_by_func (self->monitor, directory_changed, self); |
243 | g_clear_object (&self->monitor); |
244 | } |
245 | |
246 | static void |
247 | gtk_directory_list_dispose (GObject *object) |
248 | { |
249 | GtkDirectoryList *self = GTK_DIRECTORY_LIST (ptr: object); |
250 | |
251 | gtk_directory_list_stop_loading (self); |
252 | gtk_directory_list_stop_monitoring (self); |
253 | |
254 | g_clear_object (&self->file); |
255 | g_clear_pointer (&self->attributes, g_free); |
256 | |
257 | g_clear_error (err: &self->error); |
258 | g_clear_pointer (&self->items, g_sequence_free); |
259 | |
260 | g_queue_foreach (queue: &self->events, func: (GFunc) free_queued_event, NULL); |
261 | g_queue_clear (queue: &self->events); |
262 | |
263 | G_OBJECT_CLASS (gtk_directory_list_parent_class)->dispose (object); |
264 | } |
265 | |
266 | static void |
267 | gtk_directory_list_class_init (GtkDirectoryListClass *class) |
268 | { |
269 | GObjectClass *gobject_class = G_OBJECT_CLASS (class); |
270 | |
271 | gobject_class->set_property = gtk_directory_list_set_property; |
272 | gobject_class->get_property = gtk_directory_list_get_property; |
273 | gobject_class->dispose = gtk_directory_list_dispose; |
274 | |
275 | /** |
276 | * GtkDirectoryList:attributes: (attributes org.gtk.Property.get=gtk_directory_list_get_attributes org.gtk.Property.set=gtk_directory_list_set_attributes) |
277 | * |
278 | * The attributes to query. |
279 | */ |
280 | properties[PROP_ATTRIBUTES] = |
281 | g_param_spec_string (name: "attributes" , |
282 | P_("attributes" ), |
283 | P_("Attributes to query" ), |
284 | NULL, |
285 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
286 | |
287 | /** |
288 | * GtkDirectoryList:error: (attributes org.gtk.Property.get=gtk_directory_list_get_error) |
289 | * |
290 | * Error encountered while loading files. |
291 | */ |
292 | properties[PROP_ERROR] = |
293 | g_param_spec_boxed (name: "error" , |
294 | P_("error" ), |
295 | P_("Error encountered while loading files" ), |
296 | G_TYPE_ERROR, |
297 | GTK_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); |
298 | |
299 | /** |
300 | * GtkDirectoryList:file: (attributes org.gtk.Property.get=gtk_directory_list_get_file org.gtk.Property.set=gtk_directory_list_set_file) |
301 | * |
302 | * File to query. |
303 | */ |
304 | properties[PROP_FILE] = |
305 | g_param_spec_object (name: "file" , |
306 | P_("File" ), |
307 | P_("The file to query" ), |
308 | G_TYPE_FILE, |
309 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
310 | |
311 | /** |
312 | * GtkDirectoryList:io-priority: (attributes org.gtk.Property.get=gtk_directory_list_get_io_priority org.gtk.Property.set=gtk_directory_list_set_io_priority) |
313 | * |
314 | * Priority used when loading. |
315 | */ |
316 | properties[PROP_IO_PRIORITY] = |
317 | g_param_spec_int (name: "io-priority" , |
318 | P_("IO priority" ), |
319 | P_("Priority used when loading" ), |
320 | minimum: -G_MAXINT, G_MAXINT, G_PRIORITY_DEFAULT, |
321 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
322 | |
323 | /** |
324 | * GtkDirectoryList:loading: (attributes org.gtk.Property.get=gtk_directory_list_is_loading) |
325 | * |
326 | * %TRUE if files are being loaded. |
327 | */ |
328 | properties[PROP_LOADING] = |
329 | g_param_spec_boolean (name: "loading" , |
330 | P_("loading" ), |
331 | P_("TRUE if files are being loaded" ), |
332 | FALSE, |
333 | GTK_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); |
334 | |
335 | /** |
336 | * GtkDirectoryList:monitored: (attributes org.gtk.Property.get=gtk_directory_list_get_monitored org.gtk.Property.set=gtk_directory_list_set_monitored) |
337 | * |
338 | * %TRUE if the directory is monitored for changed. |
339 | */ |
340 | properties[PROP_MONITORED] = |
341 | g_param_spec_boolean (name: "monitored" , |
342 | P_("monitored" ), |
343 | P_("TRUE if the directory is monitored for changes" ), |
344 | TRUE, |
345 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
346 | |
347 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: NUM_PROPERTIES, pspecs: properties); |
348 | } |
349 | |
350 | static void |
351 | gtk_directory_list_init (GtkDirectoryList *self) |
352 | { |
353 | self->items = g_sequence_new (data_destroy: g_object_unref); |
354 | self->io_priority = G_PRIORITY_DEFAULT; |
355 | self->monitored = TRUE; |
356 | g_queue_init (queue: &self->events); |
357 | } |
358 | |
359 | /** |
360 | * gtk_directory_list_new: |
361 | * @file: (nullable): The file to query |
362 | * @attributes: (nullable): The attributes to query with |
363 | * |
364 | * Creates a new `GtkDirectoryList`. |
365 | * |
366 | * The `GtkDirectoryList` is querying the given @file |
367 | * with the given @attributes. |
368 | * |
369 | * Returns: a new `GtkDirectoryList` |
370 | **/ |
371 | GtkDirectoryList * |
372 | gtk_directory_list_new (const char *attributes, |
373 | GFile *file) |
374 | { |
375 | g_return_val_if_fail (file == NULL || G_IS_FILE (file), NULL); |
376 | |
377 | return g_object_new (GTK_TYPE_DIRECTORY_LIST, |
378 | first_property_name: "attributes" , attributes, |
379 | "file" , file, |
380 | NULL); |
381 | } |
382 | |
383 | static void |
384 | gtk_directory_list_clear_items (GtkDirectoryList *self) |
385 | { |
386 | guint n_items; |
387 | |
388 | n_items = g_sequence_get_length (seq: self->items); |
389 | if (n_items > 0) |
390 | { |
391 | g_sequence_remove_range (begin: g_sequence_get_begin_iter (seq: self->items), |
392 | end: g_sequence_get_end_iter (seq: self->items)); |
393 | |
394 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: 0, removed: n_items, added: 0); |
395 | } |
396 | |
397 | if (self->error) |
398 | { |
399 | g_clear_error (err: &self->error); |
400 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ERROR]); |
401 | } |
402 | } |
403 | |
404 | static void |
405 | gtk_directory_list_enumerator_closed_cb (GObject *source, |
406 | GAsyncResult *res, |
407 | gpointer user_data) |
408 | { |
409 | g_file_enumerator_close_finish (G_FILE_ENUMERATOR (source), result: res, NULL); |
410 | } |
411 | |
412 | static void |
413 | gtk_directory_list_got_files_cb (GObject *source, |
414 | GAsyncResult *res, |
415 | gpointer user_data) |
416 | { |
417 | GtkDirectoryList *self = user_data; /* invalid if cancelled */ |
418 | GFileEnumerator *enumerator = G_FILE_ENUMERATOR (source); |
419 | GError *error = NULL; |
420 | GList *l, *files; |
421 | guint n; |
422 | |
423 | files = g_file_enumerator_next_files_finish (enumerator, result: res, error: &error); |
424 | |
425 | if (files == NULL) |
426 | { |
427 | if (g_error_matches (error, G_IO_ERROR, code: G_IO_ERROR_CANCELLED)) |
428 | { |
429 | g_clear_error (err: &error); |
430 | return; |
431 | } |
432 | |
433 | g_file_enumerator_close_async (enumerator, |
434 | io_priority: self->io_priority, |
435 | NULL, |
436 | callback: gtk_directory_list_enumerator_closed_cb, |
437 | NULL); |
438 | |
439 | g_object_freeze_notify (G_OBJECT (self)); |
440 | |
441 | g_clear_object (&self->cancellable); |
442 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LOADING]); |
443 | |
444 | if (error) |
445 | { |
446 | self->error = error; |
447 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ERROR]); |
448 | } |
449 | |
450 | g_object_thaw_notify (G_OBJECT (self)); |
451 | return; |
452 | } |
453 | |
454 | n = 0; |
455 | for (l = files; l; l = l->next) |
456 | { |
457 | GFileInfo *info; |
458 | GFile *file; |
459 | |
460 | info = l->data; |
461 | file = g_file_enumerator_get_child (enumerator, info); |
462 | g_file_info_set_attribute_object (info, attribute: "standard::file" , G_OBJECT (file)); |
463 | g_object_unref (object: file); |
464 | g_sequence_append (seq: self->items, data: info); |
465 | n++; |
466 | } |
467 | g_list_free (list: files); |
468 | |
469 | g_file_enumerator_next_files_async (enumerator, |
470 | num_files: g_file_is_native (file: self->file) ? 50 * FILES_PER_QUERY : FILES_PER_QUERY, |
471 | io_priority: self->io_priority, |
472 | cancellable: self->cancellable, |
473 | callback: gtk_directory_list_got_files_cb, |
474 | user_data: self); |
475 | |
476 | if (n > 0) |
477 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: g_sequence_get_length (seq: self->items) - n, removed: 0, added: n); |
478 | } |
479 | |
480 | static void |
481 | gtk_directory_list_got_enumerator_cb (GObject *source, |
482 | GAsyncResult *res, |
483 | gpointer user_data) |
484 | { |
485 | GtkDirectoryList *self = user_data; /* invalid if cancelled */ |
486 | GFile *file = G_FILE (source); |
487 | GFileEnumerator *enumerator; |
488 | GError *error = NULL; |
489 | |
490 | enumerator = g_file_enumerate_children_finish (file, res, error: &error); |
491 | if (enumerator == NULL) |
492 | { |
493 | if (g_error_matches (error, G_IO_ERROR, code: G_IO_ERROR_CANCELLED)) |
494 | { |
495 | g_clear_error (err: &error); |
496 | return; |
497 | } |
498 | |
499 | g_object_freeze_notify (G_OBJECT (self)); |
500 | self->error = error; |
501 | g_clear_object (&self->cancellable); |
502 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LOADING]); |
503 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ERROR]); |
504 | g_object_thaw_notify (G_OBJECT (self)); |
505 | return; |
506 | } |
507 | |
508 | g_file_enumerator_next_files_async (enumerator, |
509 | num_files: g_file_is_native (file) ? 50 * FILES_PER_QUERY : FILES_PER_QUERY, |
510 | io_priority: self->io_priority, |
511 | cancellable: self->cancellable, |
512 | callback: gtk_directory_list_got_files_cb, |
513 | user_data: self); |
514 | g_object_unref (object: enumerator); |
515 | } |
516 | |
517 | static void |
518 | gtk_directory_list_start_loading (GtkDirectoryList *self) |
519 | { |
520 | gboolean was_loading; |
521 | |
522 | was_loading = gtk_directory_list_stop_loading (self); |
523 | gtk_directory_list_clear_items (self); |
524 | |
525 | if (self->file == NULL) |
526 | { |
527 | if (was_loading) |
528 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LOADING]); |
529 | return; |
530 | } |
531 | |
532 | self->cancellable = g_cancellable_new (); |
533 | g_file_enumerate_children_async (file: self->file, |
534 | attributes: self->attributes, |
535 | flags: G_FILE_QUERY_INFO_NONE, |
536 | io_priority: self->io_priority, |
537 | cancellable: self->cancellable, |
538 | callback: gtk_directory_list_got_enumerator_cb, |
539 | user_data: self); |
540 | |
541 | if (!was_loading) |
542 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LOADING]); |
543 | } |
544 | |
545 | static GSequenceIter * |
546 | find_file (GSequence *sequence, |
547 | GFile *file) |
548 | { |
549 | GSequenceIter *iter; |
550 | |
551 | for (iter = g_sequence_get_begin_iter (seq: sequence); |
552 | !g_sequence_iter_is_end (iter); |
553 | iter = g_sequence_iter_next (iter)) |
554 | { |
555 | GFileInfo *item = G_FILE_INFO (g_sequence_get (iter)); |
556 | GFile *f = G_FILE (g_file_info_get_attribute_object (item, "standard::file" )); |
557 | |
558 | if (g_file_equal (file1: f, file2: file)) |
559 | return iter; |
560 | } |
561 | |
562 | return NULL; |
563 | } |
564 | |
565 | static gboolean |
566 | handle_event (QueuedEvent *event) |
567 | { |
568 | GtkDirectoryList *self = event->list; |
569 | GFile *file = event->file; |
570 | GFileInfo *info = event->info; |
571 | GSequenceIter *iter; |
572 | unsigned int position; |
573 | |
574 | switch ((int)event->event) |
575 | { |
576 | case G_FILE_MONITOR_EVENT_MOVED_IN: |
577 | case G_FILE_MONITOR_EVENT_CREATED: |
578 | if (!info) |
579 | return FALSE; |
580 | |
581 | g_file_info_set_attribute_object (info, attribute: "standard::file" , G_OBJECT (file)); |
582 | |
583 | iter = find_file (sequence: self->items, file); |
584 | if (iter) |
585 | { |
586 | position = g_sequence_iter_get_position (iter); |
587 | g_sequence_set (iter, g_object_ref (info)); |
588 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position, removed: 1, added: 1); |
589 | } |
590 | else |
591 | { |
592 | position = g_sequence_get_length (seq: self->items); |
593 | g_sequence_append (seq: self->items, g_object_ref (info)); |
594 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position, removed: 0, added: 1); |
595 | } |
596 | break; |
597 | |
598 | case G_FILE_MONITOR_EVENT_MOVED_OUT: |
599 | case G_FILE_MONITOR_EVENT_DELETED: |
600 | iter = find_file (sequence: self->items, file); |
601 | if (iter) |
602 | { |
603 | position = g_sequence_iter_get_position (iter); |
604 | g_sequence_remove (iter); |
605 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position, removed: 1, added: 0); |
606 | } |
607 | break; |
608 | |
609 | case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED: |
610 | if (!info) |
611 | return FALSE; |
612 | |
613 | g_file_info_set_attribute_object (info, attribute: "standard::file" , G_OBJECT (file)); |
614 | |
615 | iter = find_file (sequence: self->items, file); |
616 | if (iter) |
617 | { |
618 | position = g_sequence_iter_get_position (iter); |
619 | g_sequence_set (iter, g_object_ref (info)); |
620 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position, removed: 1, added: 1); |
621 | } |
622 | break; |
623 | |
624 | default: |
625 | g_assert_not_reached (); |
626 | } |
627 | |
628 | return TRUE; |
629 | } |
630 | |
631 | static void |
632 | handle_events (GtkDirectoryList *self) |
633 | { |
634 | QueuedEvent *event; |
635 | |
636 | do |
637 | { |
638 | event = g_queue_peek_tail (queue: &self->events); |
639 | if (!event) |
640 | return; |
641 | |
642 | if (!handle_event (event)) |
643 | return; |
644 | |
645 | event = g_queue_pop_tail (queue: &self->events); |
646 | free_queued_event (data: event); |
647 | } |
648 | while (TRUE); |
649 | } |
650 | |
651 | static void |
652 | got_new_file_info_cb (GObject *source, |
653 | GAsyncResult *res, |
654 | gpointer data) |
655 | { |
656 | QueuedEvent *event = data; |
657 | GtkDirectoryList *self = event->list; |
658 | GFile *file = event->file; |
659 | |
660 | event->info = g_file_query_info_finish (file, res, NULL); |
661 | handle_events (self); |
662 | } |
663 | |
664 | static void |
665 | got_existing_file_info_cb (GObject *source, |
666 | GAsyncResult *res, |
667 | gpointer data) |
668 | { |
669 | QueuedEvent *event = data; |
670 | GtkDirectoryList *self = event->list; |
671 | GFile *file = event->file; |
672 | |
673 | event->info = g_file_query_info_finish (file, res, NULL); |
674 | handle_events (self); |
675 | } |
676 | |
677 | static void |
678 | directory_changed (GFileMonitor *monitor, |
679 | GFile *file, |
680 | GFile *other_file, |
681 | GFileMonitorEvent event, |
682 | gpointer data) |
683 | { |
684 | GtkDirectoryList *self = GTK_DIRECTORY_LIST (ptr: data); |
685 | QueuedEvent *ev; |
686 | |
687 | switch (event) |
688 | { |
689 | case G_FILE_MONITOR_EVENT_MOVED_IN: |
690 | case G_FILE_MONITOR_EVENT_CREATED: |
691 | ev = g_new0 (QueuedEvent, 1); |
692 | ev->list = self; |
693 | ev->event = event; |
694 | ev->file = g_object_ref (file); |
695 | g_queue_push_head (queue: &self->events, data: ev); |
696 | |
697 | g_file_query_info_async (file, |
698 | attributes: self->attributes, |
699 | flags: G_FILE_QUERY_INFO_NONE, |
700 | io_priority: self->io_priority, |
701 | cancellable: self->cancellable, |
702 | callback: got_new_file_info_cb, |
703 | user_data: ev); |
704 | break; |
705 | |
706 | case G_FILE_MONITOR_EVENT_MOVED_OUT: |
707 | case G_FILE_MONITOR_EVENT_DELETED: |
708 | ev = g_new0 (QueuedEvent, 1); |
709 | ev->list = self; |
710 | ev->event = event; |
711 | ev->file = g_object_ref (file); |
712 | g_queue_push_head (queue: &self->events, data: ev); |
713 | |
714 | handle_events (self); |
715 | break; |
716 | |
717 | case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED: |
718 | ev = g_new0 (QueuedEvent, 1); |
719 | ev->list = self; |
720 | ev->event = event; |
721 | ev->file = g_object_ref (file); |
722 | g_queue_push_head (queue: &self->events, data: ev); |
723 | |
724 | g_file_query_info_async (file, |
725 | attributes: self->attributes, |
726 | flags: G_FILE_QUERY_INFO_NONE, |
727 | io_priority: self->io_priority, |
728 | cancellable: self->cancellable, |
729 | callback: got_existing_file_info_cb, |
730 | user_data: ev); |
731 | break; |
732 | |
733 | case G_FILE_MONITOR_EVENT_RENAMED: |
734 | ev = g_new0 (QueuedEvent, 1); |
735 | ev->list = self; |
736 | ev->event = G_FILE_MONITOR_EVENT_DELETED; |
737 | ev->file = g_object_ref (file); |
738 | g_queue_push_head (queue: &self->events, data: ev); |
739 | |
740 | ev = g_new0 (QueuedEvent, 1); |
741 | ev->list = self; |
742 | ev->event = G_FILE_MONITOR_EVENT_CREATED; |
743 | ev->file = g_object_ref (other_file); |
744 | g_queue_push_head (queue: &self->events, data: ev); |
745 | |
746 | g_file_query_info_async (file: other_file, |
747 | attributes: self->attributes, |
748 | flags: G_FILE_QUERY_INFO_NONE, |
749 | io_priority: self->io_priority, |
750 | cancellable: self->cancellable, |
751 | callback: got_existing_file_info_cb, |
752 | user_data: ev); |
753 | break; |
754 | |
755 | case G_FILE_MONITOR_EVENT_CHANGED: |
756 | case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT: |
757 | case G_FILE_MONITOR_EVENT_PRE_UNMOUNT: |
758 | case G_FILE_MONITOR_EVENT_UNMOUNTED: |
759 | case G_FILE_MONITOR_EVENT_MOVED: |
760 | default: |
761 | break; |
762 | } |
763 | } |
764 | |
765 | static void |
766 | gtk_directory_list_start_monitoring (GtkDirectoryList *self) |
767 | { |
768 | g_assert (self->monitor == NULL); |
769 | self->monitor = g_file_monitor_directory (file: self->file, flags: G_FILE_MONITOR_WATCH_MOVES, NULL, NULL); |
770 | g_signal_connect (self->monitor, "changed" , G_CALLBACK (directory_changed), self); |
771 | } |
772 | |
773 | static void |
774 | gtk_directory_list_update_monitoring (GtkDirectoryList *self) |
775 | { |
776 | gtk_directory_list_stop_monitoring (self); |
777 | if (self->file && self->monitored) |
778 | gtk_directory_list_start_monitoring (self); |
779 | } |
780 | |
781 | /** |
782 | * gtk_directory_list_set_file: (attributes org.gtk.Method.set_property=file) |
783 | * @self: a `GtkDirectoryList` |
784 | * @file: (nullable): the `GFile` to be enumerated |
785 | * |
786 | * Sets the @file to be enumerated and starts the enumeration. |
787 | * |
788 | * If @file is %NULL, the result will be an empty list. |
789 | */ |
790 | void |
791 | gtk_directory_list_set_file (GtkDirectoryList *self, |
792 | GFile *file) |
793 | { |
794 | g_return_if_fail (GTK_IS_DIRECTORY_LIST (self)); |
795 | g_return_if_fail (file == NULL || G_IS_FILE (file)); |
796 | |
797 | if (self->file == file || |
798 | (self->file && file && g_file_equal (file1: self->file, file2: file))) |
799 | return; |
800 | |
801 | g_object_freeze_notify (G_OBJECT (self)); |
802 | |
803 | g_set_object (&self->file, file); |
804 | |
805 | gtk_directory_list_update_monitoring (self); |
806 | gtk_directory_list_start_loading (self); |
807 | |
808 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_FILE]); |
809 | |
810 | g_object_thaw_notify (G_OBJECT (self)); |
811 | } |
812 | |
813 | /** |
814 | * gtk_directory_list_get_file: (attributes org.gtk.Method.get_property=file) |
815 | * @self: a `GtkDirectoryList` |
816 | * |
817 | * Gets the file whose children are currently enumerated. |
818 | * |
819 | * Returns: (nullable) (transfer none): The file whose children are enumerated |
820 | **/ |
821 | GFile * |
822 | gtk_directory_list_get_file (GtkDirectoryList *self) |
823 | { |
824 | g_return_val_if_fail (GTK_IS_DIRECTORY_LIST (self), NULL); |
825 | |
826 | return self->file; |
827 | } |
828 | |
829 | /** |
830 | * gtk_directory_list_set_attributes: (attributes org.gtk.Method.set_property=attributes) |
831 | * @self: a `GtkDirectoryList` |
832 | * @attributes: (nullable): the attributes to enumerate |
833 | * |
834 | * Sets the @attributes to be enumerated and starts the enumeration. |
835 | * |
836 | * If @attributes is %NULL, no attributes will be queried, but a list |
837 | * of `GFileInfo`s will still be created. |
838 | */ |
839 | void |
840 | gtk_directory_list_set_attributes (GtkDirectoryList *self, |
841 | const char *attributes) |
842 | { |
843 | g_return_if_fail (GTK_IS_DIRECTORY_LIST (self)); |
844 | |
845 | if (self->attributes == attributes) |
846 | return; |
847 | |
848 | g_object_freeze_notify (G_OBJECT (self)); |
849 | |
850 | g_free (mem: self->attributes); |
851 | self->attributes = g_strdup (str: attributes); |
852 | |
853 | gtk_directory_list_start_loading (self); |
854 | |
855 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ATTRIBUTES]); |
856 | |
857 | g_object_thaw_notify (G_OBJECT (self)); |
858 | } |
859 | |
860 | /** |
861 | * gtk_directory_list_get_attributes: (attributes org.gtk.Method.get_property=attributes) |
862 | * @self: a `GtkDirectoryList` |
863 | * |
864 | * Gets the attributes queried on the children. |
865 | * |
866 | * Returns: (nullable) (transfer none): The queried attributes |
867 | */ |
868 | const char * |
869 | gtk_directory_list_get_attributes (GtkDirectoryList *self) |
870 | { |
871 | g_return_val_if_fail (GTK_IS_DIRECTORY_LIST (self), NULL); |
872 | |
873 | return self->attributes; |
874 | } |
875 | |
876 | /** |
877 | * gtk_directory_list_set_io_priority: (attributes org.gtk.Method.set_property=io-priority) |
878 | * @self: a `GtkDirectoryList` |
879 | * @io_priority: IO priority to use |
880 | * |
881 | * Sets the IO priority to use while loading directories. |
882 | * |
883 | * Setting the priority while @self is loading will reprioritize the |
884 | * ongoing load as soon as possible. |
885 | * |
886 | * The default IO priority is %G_PRIORITY_DEFAULT, which is higher than |
887 | * the GTK redraw priority. If you are loading a lot of directories in |
888 | * parallel, lowering it to something like %G_PRIORITY_DEFAULT_IDLE |
889 | * may increase responsiveness. |
890 | */ |
891 | void |
892 | gtk_directory_list_set_io_priority (GtkDirectoryList *self, |
893 | int io_priority) |
894 | { |
895 | g_return_if_fail (GTK_IS_DIRECTORY_LIST (self)); |
896 | |
897 | if (self->io_priority == io_priority) |
898 | return; |
899 | |
900 | self->io_priority = io_priority; |
901 | |
902 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_IO_PRIORITY]); |
903 | } |
904 | |
905 | /** |
906 | * gtk_directory_list_get_io_priority: (attributes org.gtk.Method.get_property=io-priority) |
907 | * @self: a `GtkDirectoryList` |
908 | * |
909 | * Gets the IO priority set via gtk_directory_list_set_io_priority(). |
910 | * |
911 | * Returns: The IO priority. |
912 | */ |
913 | int |
914 | gtk_directory_list_get_io_priority (GtkDirectoryList *self) |
915 | { |
916 | g_return_val_if_fail (GTK_IS_DIRECTORY_LIST (self), G_PRIORITY_DEFAULT); |
917 | |
918 | return self->io_priority; |
919 | } |
920 | |
921 | /** |
922 | * gtk_directory_list_is_loading: (attributes org.gtk.Method.get_property=loading) |
923 | * @self: a `GtkDirectoryList` |
924 | * |
925 | * Returns %TRUE if the children enumeration is currently in |
926 | * progress. |
927 | * |
928 | * Files will be added to @self from time to time while loading is |
929 | * going on. The order in which are added is undefined and may change |
930 | * in between runs. |
931 | * |
932 | * Returns: %TRUE if @self is loading |
933 | */ |
934 | gboolean |
935 | gtk_directory_list_is_loading (GtkDirectoryList *self) |
936 | { |
937 | g_return_val_if_fail (GTK_IS_DIRECTORY_LIST (self), FALSE); |
938 | |
939 | return self->cancellable != NULL; |
940 | } |
941 | |
942 | /** |
943 | * gtk_directory_list_get_error: (attributes org.gtk.Method.get_property=error) |
944 | * @self: a `GtkDirectoryList` |
945 | * |
946 | * Gets the loading error, if any. |
947 | * |
948 | * If an error occurs during the loading process, the loading process |
949 | * will finish and this property allows querying the error that happened. |
950 | * This error will persist until a file is loaded again. |
951 | * |
952 | * An error being set does not mean that no files were loaded, and all |
953 | * successfully queried files will remain in the list. |
954 | * |
955 | * Returns: (nullable) (transfer none): The loading error or %NULL if |
956 | * loading finished successfully |
957 | */ |
958 | const GError * |
959 | gtk_directory_list_get_error (GtkDirectoryList *self) |
960 | { |
961 | g_return_val_if_fail (GTK_IS_DIRECTORY_LIST (self), FALSE); |
962 | |
963 | return self->error; |
964 | } |
965 | |
966 | /** |
967 | * gtk_directory_list_set_monitored: (attributes org.gtk.Method.set_property=monitored) |
968 | * @self: a `GtkDirectoryList` |
969 | * @monitored: %TRUE to monitor the directory for changes |
970 | * |
971 | * Sets whether the directory list will monitor the directory |
972 | * for changes. |
973 | * |
974 | * If monitoring is enabled, the ::items-changed signal will |
975 | * be emitted when the directory contents change. |
976 | * |
977 | * |
978 | * When monitoring is turned on after the initial creation |
979 | * of the directory list, the directory is reloaded to avoid |
980 | * missing files that appeared between the initial loading |
981 | * and when monitoring was turned on. |
982 | */ |
983 | void |
984 | gtk_directory_list_set_monitored (GtkDirectoryList *self, |
985 | gboolean monitored) |
986 | { |
987 | g_return_if_fail (GTK_IS_DIRECTORY_LIST (self)); |
988 | |
989 | if (self->monitored == monitored) |
990 | return; |
991 | |
992 | self->monitored = monitored; |
993 | |
994 | gtk_directory_list_update_monitoring (self); |
995 | if (monitored) |
996 | gtk_directory_list_start_loading (self); |
997 | |
998 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_MONITORED]); |
999 | } |
1000 | |
1001 | /** |
1002 | * gtk_directory_list_get_monitored: (attributes org.gtk.Method.get_property=monitored) |
1003 | * @self: a `GtkDirectoryList` |
1004 | * |
1005 | * Returns whether the directory list is monitoring |
1006 | * the directory for changes. |
1007 | * |
1008 | * Returns: %TRUE if the directory is monitored |
1009 | */ |
1010 | gboolean |
1011 | gtk_directory_list_get_monitored (GtkDirectoryList *self) |
1012 | { |
1013 | g_return_val_if_fail (GTK_IS_DIRECTORY_LIST (self), TRUE); |
1014 | |
1015 | return self->monitored; |
1016 | } |
1017 | |