1 | /* |
2 | * Copyright © 2020 Red Hat, Inc. |
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: Matthias Clasen <mclasen@redhat.com> |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include "gtkbookmarklist.h" |
23 | |
24 | #include "gtksettings.h" |
25 | #include "gtkintl.h" |
26 | #include "gtkprivate.h" |
27 | |
28 | /** |
29 | * GtkBookmarkList: |
30 | * |
31 | * `GtkBookmarkList` is a list model that wraps `GBookmarkFile`. |
32 | * |
33 | * It presents a `GListModel` and fills it asynchronously with the |
34 | * `GFileInfo`s returned from that function. |
35 | * |
36 | * The `GFileInfo`s in the list have some attributes in the recent |
37 | * namespace added: `recent::private` (boolean) and `recent:applications` |
38 | * (stringv). |
39 | */ |
40 | |
41 | enum { |
42 | PROP_0, |
43 | PROP_FILENAME, |
44 | PROP_ATTRIBUTES, |
45 | PROP_IO_PRIORITY, |
46 | PROP_LOADING, |
47 | NUM_PROPERTIES |
48 | }; |
49 | |
50 | struct _GtkBookmarkList |
51 | { |
52 | GObject parent_instance; |
53 | |
54 | char *attributes; |
55 | char *filename; |
56 | int io_priority; |
57 | int loading; |
58 | |
59 | GCancellable *cancellable; |
60 | GFileMonitor *monitor; |
61 | GBookmarkFile *file; |
62 | |
63 | GSequence *items; |
64 | }; |
65 | |
66 | struct _GtkBookmarkListClass |
67 | { |
68 | GObjectClass parent_class; |
69 | }; |
70 | |
71 | static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; |
72 | |
73 | static GType |
74 | gtk_bookmark_list_get_item_type (GListModel *list) |
75 | { |
76 | return G_TYPE_FILE_INFO; |
77 | } |
78 | |
79 | static guint |
80 | gtk_bookmark_list_get_n_items (GListModel *list) |
81 | { |
82 | GtkBookmarkList *self = GTK_BOOKMARK_LIST (ptr: list); |
83 | |
84 | return g_sequence_get_length (seq: self->items); |
85 | } |
86 | |
87 | static gpointer |
88 | gtk_bookmark_list_get_item (GListModel *list, |
89 | guint position) |
90 | { |
91 | GtkBookmarkList *self = GTK_BOOKMARK_LIST (ptr: list); |
92 | GSequenceIter *iter; |
93 | |
94 | iter = g_sequence_get_iter_at_pos (seq: self->items, pos: position); |
95 | |
96 | if (g_sequence_iter_is_end (iter)) |
97 | return NULL; |
98 | else |
99 | return g_object_ref (g_sequence_get (iter)); |
100 | } |
101 | |
102 | static void |
103 | gtk_bookmark_list_model_init (GListModelInterface *iface) |
104 | { |
105 | iface->get_item_type = gtk_bookmark_list_get_item_type; |
106 | iface->get_n_items = gtk_bookmark_list_get_n_items; |
107 | iface->get_item = gtk_bookmark_list_get_item; |
108 | } |
109 | |
110 | static void gtk_bookmark_list_start_loading (GtkBookmarkList *self); |
111 | static gboolean gtk_bookmark_list_stop_loading (GtkBookmarkList *self); |
112 | static void bookmark_file_changed (GFileMonitor *monitor, |
113 | GFile *file, |
114 | GFile *other_file, |
115 | GFileMonitorEvent event, |
116 | gpointer data); |
117 | static void gtk_bookmark_list_set_filename (GtkBookmarkList *self, |
118 | const char *filename); |
119 | |
120 | G_DEFINE_TYPE_WITH_CODE (GtkBookmarkList, gtk_bookmark_list, G_TYPE_OBJECT, |
121 | G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_bookmark_list_model_init)) |
122 | |
123 | static void |
124 | gtk_bookmark_list_set_property (GObject *object, |
125 | guint prop_id, |
126 | const GValue *value, |
127 | GParamSpec *pspec) |
128 | { |
129 | GtkBookmarkList *self = GTK_BOOKMARK_LIST (ptr: object); |
130 | |
131 | switch (prop_id) |
132 | { |
133 | case PROP_ATTRIBUTES: |
134 | gtk_bookmark_list_set_attributes (self, attributes: g_value_get_string (value)); |
135 | break; |
136 | |
137 | case PROP_IO_PRIORITY: |
138 | gtk_bookmark_list_set_io_priority (self, io_priority: g_value_get_int (value)); |
139 | break; |
140 | |
141 | case PROP_FILENAME: |
142 | gtk_bookmark_list_set_filename (self, filename: g_value_get_string (value)); |
143 | break; |
144 | |
145 | default: |
146 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
147 | break; |
148 | } |
149 | } |
150 | |
151 | static void |
152 | gtk_bookmark_list_get_property (GObject *object, |
153 | guint prop_id, |
154 | GValue *value, |
155 | GParamSpec *pspec) |
156 | { |
157 | GtkBookmarkList *self = GTK_BOOKMARK_LIST (ptr: object); |
158 | |
159 | switch (prop_id) |
160 | { |
161 | case PROP_ATTRIBUTES: |
162 | g_value_set_string (value, v_string: self->attributes); |
163 | break; |
164 | |
165 | case PROP_IO_PRIORITY: |
166 | g_value_set_int (value, v_int: self->io_priority); |
167 | break; |
168 | |
169 | case PROP_FILENAME: |
170 | g_value_set_string (value, v_string: self->filename); |
171 | break; |
172 | |
173 | case PROP_LOADING: |
174 | g_value_set_boolean (value, v_boolean: gtk_bookmark_list_is_loading (self)); |
175 | break; |
176 | |
177 | default: |
178 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
179 | break; |
180 | } |
181 | } |
182 | |
183 | static void |
184 | gtk_bookmark_list_dispose (GObject *object) |
185 | { |
186 | GtkBookmarkList *self = GTK_BOOKMARK_LIST (ptr: object); |
187 | |
188 | gtk_bookmark_list_stop_loading (self); |
189 | |
190 | g_clear_pointer (&self->attributes, g_free); |
191 | g_clear_pointer (&self->filename, g_free); |
192 | g_clear_pointer (&self->items, g_sequence_free); |
193 | g_clear_pointer (&self->file, g_bookmark_file_free); |
194 | |
195 | g_signal_handlers_disconnect_by_func (self->monitor, G_CALLBACK (bookmark_file_changed), self); |
196 | g_clear_object (&self->monitor); |
197 | |
198 | G_OBJECT_CLASS (gtk_bookmark_list_parent_class)->dispose (object); |
199 | } |
200 | |
201 | static void |
202 | gtk_bookmark_list_class_init (GtkBookmarkListClass *class) |
203 | { |
204 | GObjectClass *gobject_class = G_OBJECT_CLASS (class); |
205 | |
206 | gobject_class->set_property = gtk_bookmark_list_set_property; |
207 | gobject_class->get_property = gtk_bookmark_list_get_property; |
208 | gobject_class->dispose = gtk_bookmark_list_dispose; |
209 | |
210 | /** |
211 | * GtkBookmarkList:filename: (attributes org.gtk.Property.get=gtk_bookmark_list_get_filename) |
212 | * |
213 | * The bookmark file to load. |
214 | */ |
215 | properties[PROP_FILENAME] = |
216 | g_param_spec_string (name: "filename" , |
217 | P_("Filename" ), |
218 | P_("Bookmark file to load" ), |
219 | NULL, |
220 | GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY); |
221 | /** |
222 | * GtkBookmarkList:attributes: (attributes org.gtk.Property.get=gtk_bookmark_list_get_attributes org.gtk.Property.set=gtk_bookmark_list_set_attributes) |
223 | * |
224 | * The attributes to query. |
225 | */ |
226 | properties[PROP_ATTRIBUTES] = |
227 | g_param_spec_string (name: "attributes" , |
228 | P_("Attributes" ), |
229 | P_("Attributes to query" ), |
230 | NULL, |
231 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
232 | |
233 | /** |
234 | * GtkBookmarkList:io-priority: (attributes org.gtk.Property.get=gtk_bookmark_list_get_io_priority org.gtk.Property.set=gtk_bookmark_list_set_io_priority) |
235 | * |
236 | * Priority used when loading. |
237 | */ |
238 | properties[PROP_IO_PRIORITY] = |
239 | g_param_spec_int (name: "io-priority" , |
240 | P_("IO priority" ), |
241 | P_("Priority used when loading" ), |
242 | minimum: -G_MAXINT, G_MAXINT, G_PRIORITY_DEFAULT, |
243 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
244 | |
245 | /** |
246 | * GtkBookmarkList:loading: (attributes org.gtk.Property.get=gtk_bookmark_list_is_loading) |
247 | * |
248 | * %TRUE if files are being loaded. |
249 | */ |
250 | properties[PROP_LOADING] = |
251 | g_param_spec_boolean (name: "loading" , |
252 | P_("loading" ), |
253 | P_("TRUE if files are being loaded" ), |
254 | FALSE, |
255 | GTK_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); |
256 | |
257 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: NUM_PROPERTIES, pspecs: properties); |
258 | } |
259 | |
260 | static void |
261 | gtk_bookmark_list_init (GtkBookmarkList *self) |
262 | { |
263 | self->items = g_sequence_new (data_destroy: g_object_unref); |
264 | self->io_priority = G_PRIORITY_DEFAULT; |
265 | self->file = g_bookmark_file_new (); |
266 | } |
267 | |
268 | static gboolean |
269 | gtk_bookmark_list_stop_loading (GtkBookmarkList *self) |
270 | { |
271 | if (self->cancellable == NULL) |
272 | return FALSE; |
273 | |
274 | g_cancellable_cancel (cancellable: self->cancellable); |
275 | g_clear_object (&self->cancellable); |
276 | |
277 | self->loading = 0; |
278 | |
279 | return TRUE; |
280 | } |
281 | |
282 | static void |
283 | got_file_info (GObject *source, |
284 | GAsyncResult *res, |
285 | gpointer user_data) |
286 | { |
287 | GtkBookmarkList *self = user_data; |
288 | GFile *file = G_FILE (source); |
289 | GFileInfo *info; |
290 | GError *error = NULL; |
291 | |
292 | info = g_file_query_info_finish (file, res, error: &error); |
293 | if (g_error_matches (error, G_IO_ERROR, code: G_IO_ERROR_CANCELLED)) |
294 | { |
295 | g_error_free (error); |
296 | return; |
297 | } |
298 | |
299 | if (info) |
300 | { |
301 | char *uri; |
302 | gboolean is_private; |
303 | char **apps; |
304 | |
305 | uri = g_file_get_uri (file); |
306 | is_private = g_bookmark_file_get_is_private (bookmark: self->file, uri, NULL); |
307 | apps = g_bookmark_file_get_applications (bookmark: self->file, uri, NULL, NULL); |
308 | |
309 | g_file_info_set_attribute_object (info, attribute: "standard::file" , G_OBJECT (file)); |
310 | g_file_info_set_attribute_boolean (info, attribute: "recent::private" , attr_value: is_private); |
311 | g_file_info_set_attribute_stringv (info, attribute: "recent::applications" , attr_value: apps); |
312 | |
313 | g_strfreev (str_array: apps); |
314 | |
315 | g_sequence_append (seq: self->items, data: info); |
316 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: g_sequence_get_length (seq: self->items) - 1, removed: 0, added: 1); |
317 | |
318 | g_free (mem: uri); |
319 | } |
320 | |
321 | self->loading--; |
322 | |
323 | if (self->loading == 0) |
324 | { |
325 | g_clear_object (&self->cancellable); |
326 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LOADING]); |
327 | } |
328 | } |
329 | |
330 | static void |
331 | gtk_bookmark_list_clear_items (GtkBookmarkList *self) |
332 | { |
333 | guint n_items; |
334 | |
335 | n_items = g_sequence_get_length (seq: self->items); |
336 | if (n_items > 0) |
337 | { |
338 | g_sequence_remove_range (begin: g_sequence_get_begin_iter (seq: self->items), |
339 | end: g_sequence_get_end_iter (seq: self->items)); |
340 | |
341 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: 0, removed: n_items, added: 0); |
342 | } |
343 | } |
344 | |
345 | static void |
346 | gtk_bookmark_list_start_loading (GtkBookmarkList *self) |
347 | { |
348 | gboolean was_loading; |
349 | GError *error = NULL; |
350 | |
351 | was_loading = gtk_bookmark_list_stop_loading (self); |
352 | gtk_bookmark_list_clear_items (self); |
353 | |
354 | if (g_bookmark_file_load_from_file (bookmark: self->file, filename: self->filename, error: &error)) |
355 | { |
356 | char **uris; |
357 | gsize len; |
358 | int i; |
359 | |
360 | uris = g_bookmark_file_get_uris (bookmark: self->file, length: &len); |
361 | if (len > 0) |
362 | { |
363 | self->cancellable = g_cancellable_new (); |
364 | self->loading = len; |
365 | } |
366 | |
367 | for (i = 0; i < len; i++) |
368 | { |
369 | const char *uri = uris[i]; |
370 | GFile *file; |
371 | |
372 | /* add this item */ |
373 | file = g_file_new_for_uri (uri); |
374 | g_file_query_info_async (file, |
375 | attributes: self->attributes, |
376 | flags: 0, |
377 | io_priority: self->io_priority, |
378 | cancellable: self->cancellable, |
379 | callback: got_file_info, |
380 | user_data: self); |
381 | g_object_unref (object: file); |
382 | } |
383 | |
384 | g_strfreev (str_array: uris); |
385 | } |
386 | else |
387 | { |
388 | if (!g_error_matches (error, G_FILE_ERROR, code: G_FILE_ERROR_NOENT)) |
389 | g_warning ("Failed to load %s: %s" , self->filename, error->message); |
390 | g_clear_error (err: &error); |
391 | } |
392 | |
393 | if (was_loading != (self->cancellable != NULL)) |
394 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LOADING]); |
395 | } |
396 | |
397 | static void |
398 | bookmark_file_changed (GFileMonitor *monitor, |
399 | GFile *file, |
400 | GFile *other_file, |
401 | GFileMonitorEvent event_type, |
402 | gpointer data) |
403 | { |
404 | GtkBookmarkList *self = data; |
405 | |
406 | switch (event_type) |
407 | { |
408 | case G_FILE_MONITOR_EVENT_CHANGED: |
409 | case G_FILE_MONITOR_EVENT_CREATED: |
410 | case G_FILE_MONITOR_EVENT_DELETED: |
411 | gtk_bookmark_list_start_loading (self); |
412 | break; |
413 | |
414 | case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT: |
415 | case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED: |
416 | case G_FILE_MONITOR_EVENT_PRE_UNMOUNT: |
417 | case G_FILE_MONITOR_EVENT_UNMOUNTED: |
418 | case G_FILE_MONITOR_EVENT_MOVED: |
419 | case G_FILE_MONITOR_EVENT_RENAMED: |
420 | case G_FILE_MONITOR_EVENT_MOVED_IN: |
421 | case G_FILE_MONITOR_EVENT_MOVED_OUT: |
422 | |
423 | default: |
424 | break; |
425 | } |
426 | } |
427 | |
428 | static void |
429 | gtk_bookmark_list_set_filename (GtkBookmarkList *self, |
430 | const char *filename) |
431 | { |
432 | GFile *file; |
433 | |
434 | if (filename) |
435 | self->filename = g_strdup (str: filename); |
436 | else |
437 | self->filename = g_build_filename (first_element: g_get_user_data_dir (), "recently-used.xbel" , NULL); |
438 | |
439 | file = g_file_new_for_path (path: self->filename); |
440 | self->monitor = g_file_monitor_file (file, flags: G_FILE_MONITOR_NONE, NULL, NULL); |
441 | g_signal_connect (self->monitor, "changed" , |
442 | G_CALLBACK (bookmark_file_changed), self); |
443 | g_object_unref (object: file); |
444 | |
445 | gtk_bookmark_list_start_loading (self); |
446 | } |
447 | |
448 | /** |
449 | * gtk_bookmark_list_get_filename: (attributes org.gtk.Method.get_property=filename) |
450 | * @self: a `GtkBookmarkList` |
451 | * |
452 | * Returns the filename of the bookmark file that |
453 | * this list is loading. |
454 | * |
455 | * Returns: (type filename): the filename of the .xbel file |
456 | */ |
457 | const char * |
458 | gtk_bookmark_list_get_filename (GtkBookmarkList *self) |
459 | { |
460 | g_return_val_if_fail (GTK_IS_BOOKMARK_LIST (self), NULL); |
461 | |
462 | return self->filename; |
463 | } |
464 | |
465 | /** |
466 | * gtk_bookmark_list_new: |
467 | * @filename: (type filename) (nullable): The bookmark file to load |
468 | * @attributes: (nullable): The attributes to query |
469 | * |
470 | * Creates a new `GtkBookmarkList` with the given @attributes. |
471 | * |
472 | * Returns: a new `GtkBookmarkList` |
473 | */ |
474 | GtkBookmarkList * |
475 | gtk_bookmark_list_new (const char *filename, |
476 | const char *attributes) |
477 | { |
478 | return g_object_new (GTK_TYPE_BOOKMARK_LIST, |
479 | first_property_name: "filename" , filename, |
480 | "attributes" , attributes, |
481 | NULL); |
482 | } |
483 | |
484 | /** |
485 | * gtk_bookmark_list_set_attributes: (attributes org.gtk.Method.set_property=attributes) |
486 | * @self: a `GtkBookmarkList` |
487 | * @attributes: (nullable): the attributes to enumerate |
488 | * |
489 | * Sets the @attributes to be enumerated and starts the enumeration. |
490 | * |
491 | * If @attributes is %NULL, no attributes will be queried, but a list |
492 | * of `GFileInfo`s will still be created. |
493 | */ |
494 | void |
495 | gtk_bookmark_list_set_attributes (GtkBookmarkList *self, |
496 | const char *attributes) |
497 | { |
498 | g_return_if_fail (GTK_IS_BOOKMARK_LIST (self)); |
499 | |
500 | if (g_strcmp0 (str1: self->attributes, str2: attributes) == 0) |
501 | return; |
502 | |
503 | g_object_freeze_notify (G_OBJECT (self)); |
504 | |
505 | g_free (mem: self->attributes); |
506 | self->attributes = g_strdup (str: attributes); |
507 | |
508 | gtk_bookmark_list_start_loading (self); |
509 | |
510 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ATTRIBUTES]); |
511 | |
512 | g_object_thaw_notify (G_OBJECT (self)); |
513 | } |
514 | |
515 | /** |
516 | * gtk_bookmark_list_get_attributes: (attributes org.gtk.Method.get_property=attributes) |
517 | * @self: a `GtkBookmarkList` |
518 | * |
519 | * Gets the attributes queried on the children. |
520 | * |
521 | * Returns: (nullable) (transfer none): The queried attributes |
522 | */ |
523 | const char * |
524 | gtk_bookmark_list_get_attributes (GtkBookmarkList *self) |
525 | { |
526 | g_return_val_if_fail (GTK_IS_BOOKMARK_LIST (self), NULL); |
527 | |
528 | return self->attributes; |
529 | } |
530 | |
531 | /** |
532 | * gtk_bookmark_list_set_io_priority: (attributes org.gtk.Method.set_property=io-priority) |
533 | * @self: a `GtkBookmarkList` |
534 | * @io_priority: IO priority to use |
535 | * |
536 | * Sets the IO priority to use while loading files. |
537 | * |
538 | * The default IO priority is %G_PRIORITY_DEFAULT. |
539 | */ |
540 | void |
541 | gtk_bookmark_list_set_io_priority (GtkBookmarkList *self, |
542 | int io_priority) |
543 | { |
544 | g_return_if_fail (GTK_IS_BOOKMARK_LIST (self)); |
545 | |
546 | if (self->io_priority == io_priority) |
547 | return; |
548 | |
549 | self->io_priority = io_priority; |
550 | |
551 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_IO_PRIORITY]); |
552 | } |
553 | |
554 | /** |
555 | * gtk_bookmark_list_get_io_priority: (attributes org.gtk.Method.get_property=io-priority) |
556 | * @self: a `GtkBookmarkList` |
557 | * |
558 | * Gets the IO priority to use while loading file. |
559 | * |
560 | * Returns: The IO priority. |
561 | */ |
562 | int |
563 | gtk_bookmark_list_get_io_priority (GtkBookmarkList *self) |
564 | { |
565 | g_return_val_if_fail (GTK_IS_BOOKMARK_LIST (self), G_PRIORITY_DEFAULT); |
566 | |
567 | return self->io_priority; |
568 | } |
569 | |
570 | /** |
571 | * gtk_bookmark_list_is_loading: (attributes org.gtk.Method.get_property=loading) |
572 | * @self: a `GtkBookmarkList` |
573 | * |
574 | * Returns %TRUE if the files are currently being loaded. |
575 | * |
576 | * Files will be added to @self from time to time while loading is |
577 | * going on. The order in which are added is undefined and may change |
578 | * in between runs. |
579 | * |
580 | * Returns: %TRUE if @self is loading |
581 | */ |
582 | gboolean |
583 | gtk_bookmark_list_is_loading (GtkBookmarkList *self) |
584 | { |
585 | g_return_val_if_fail (GTK_IS_BOOKMARK_LIST (self), FALSE); |
586 | |
587 | return self->cancellable != NULL; |
588 | } |
589 | |