1 
2 #include <config.h>
3 
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <string.h>
7 
8 #include <sys/types.h>
9 #include <sys/stat.h>
10 #include <fcntl.h>
11 #include <unistd.h>
12 #include <utime.h>
13 
14 #include <gtk/gtk.h>
15 #include <glib/gi18n-lib.h>
16 #include <gio/gdesktopappinfo.h>
17 #include <glib/gstdio.h>
18 
19 #include "xapp-favorites.h"
20 #include "favorite-vfs-file.h"
21 
22 #define DEBUG_FLAG XAPP_DEBUG_FAVORITES
23 #include "xapp-debug.h"
24 
25 #define FAVORITES_SCHEMA "org.x.apps.favorites"
26 #define FAVORITES_KEY "list"
27 #define SETTINGS_DELIMITER "::"
28 #define MAX_DISPLAY_URI_LENGTH 20
29 
30 G_DEFINE_BOXED_TYPE (XAppFavoriteInfo, xapp_favorite_info, xapp_favorite_info_copy, xapp_favorite_info_free);
31 /**
32  * SECTION:xapp-favorites
33  * @Short_description: Keeps track of favorite files.
34  * @Title: XAppFavorites
35  *
36  * The XAppFavorites class allows applications display frequently-used files and
37  * provide a safe mechanism for launching them.
38  *
39  * A list of #XAppFavoriteInfos can be retrieved in full, or only for specific mimetypes.
40  *
41  * A favorites uri scheme is also available if the xapp gtk3 module is loaded and this also makes the
42  * uri available as a shortcut in file dialogs.
43  *
44  * XAppFavorites are new for 2.0
45  */
46 
47 /**
48  * xapp_favorite_info_copy:
49  * @info: The #XAppFavoriteInfo to duplicate.
50  *
51  * Makes an exact copy of an existing #XAppFavoriteInfo.
52  *
53  * Returns: (transfer full): a new #XAppFavoriteInfo.  Free using #xapp_favorite_info_free.
54  *
55  * Since 2.0
56  */
57 XAppFavorites *global_favorites;
58 
59 XAppFavoriteInfo *
xapp_favorite_info_copy(const XAppFavoriteInfo * info)60 xapp_favorite_info_copy (const XAppFavoriteInfo *info)
61 {
62     // DEBUG ("XAppFavoriteInfo: copy");
63     g_return_val_if_fail (info != NULL, NULL);
64 
65     XAppFavoriteInfo *_info = g_slice_dup (XAppFavoriteInfo, info);
66     _info->uri = g_strdup (info->uri);
67     _info->display_name = g_strdup (info->display_name);
68     _info->cached_mimetype = g_strdup (info->cached_mimetype);
69 
70     return _info;
71 }
72 
73 /**
74  * xapp_favorite_info_free:
75  * @info: The #XAppFavoriteInfo to free.
76  *
77  * Destroys the #XAppFavoriteInfo.
78  *
79  * Since 2.0
80  */
81 void
xapp_favorite_info_free(XAppFavoriteInfo * info)82 xapp_favorite_info_free (XAppFavoriteInfo *info)
83 {
84     DEBUG ("XAppFavoriteInfo free (%s)", info->uri);
85     g_return_if_fail (info != NULL);
86 
87     g_free (info->uri);
88     g_free (info->display_name);
89     g_free (info->cached_mimetype);
90     g_slice_free (XAppFavoriteInfo, info);
91 }
92 
93 typedef struct
94 {
95     GHashTable *infos;
96     GHashTable *menus;
97 
98     GSettings *settings;
99 
100     gulong settings_listener_id;
101     guint changed_timer_id;
102 } XAppFavoritesPrivate;
103 
104 struct _XAppFavorites
105 {
106     GObject parent_instance;
107 };
108 
109 G_DEFINE_TYPE_WITH_PRIVATE (XAppFavorites, xapp_favorites, G_TYPE_OBJECT)
110 
111 enum
112 {
113     CHANGED,
114     LAST_SIGNAL
115 };
116 
117 static guint signals[LAST_SIGNAL] = {0, };
118 
119 static void finish_add_favorite (XAppFavorites *favorites,
120                                  const gchar   *uri,
121                                  const gchar   *mimetype,
122                                  gboolean       from_saved);
123 
124 static gboolean
changed_callback(gpointer data)125 changed_callback (gpointer data)
126 {
127     g_return_val_if_fail (XAPP_IS_FAVORITES (data), G_SOURCE_REMOVE);
128     XAppFavorites *favorites = XAPP_FAVORITES (data);
129 
130     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
131     DEBUG ("XAppFavorites: list updated, emitting changed signal");
132 
133     priv->changed_timer_id = 0;
134     g_signal_emit (favorites, signals[CHANGED], 0);
135 
136     return G_SOURCE_REMOVE;
137 }
138 
139 static void
queue_changed(XAppFavorites * favorites)140 queue_changed (XAppFavorites *favorites)
141 {
142     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
143 
144     if (priv->changed_timer_id > 0)
145     {
146         g_source_remove (priv->changed_timer_id);
147     }
148 
149     priv->changed_timer_id = g_idle_add ((GSourceFunc) changed_callback, favorites);
150 }
151 
152 static void
sync_metadata_callback(GObject * source,GAsyncResult * res,gpointer user_data)153 sync_metadata_callback (GObject      *source,
154                         GAsyncResult *res,
155                         gpointer      user_data)
156 {
157     // Disabled
158     return;
159 
160 //     GFile *file;
161 //     GError *error;
162 
163 //     file = G_FILE (source);
164 //     error = NULL;
165 
166 //     if (!g_file_set_attributes_finish (file,
167 //                                        res,
168 //                                        NULL,
169 //                                        &error))
170 //     {
171 //         if (error != NULL)
172 //         {
173 //             if (error->code != G_IO_ERROR_NOT_FOUND)
174 //             {
175 //                 g_warning ("Could not update file metadata for favorite file '%s': %s", g_file_get_uri (file), error->message);
176 //             }
177 
178 //             g_error_free (error);
179 //         }
180 //     }
181 //     else
182 //     {
183 //         if (g_file_is_native (file))
184 //         {
185 //             // I can't think of any other way to touch a file so a file monitor might notice
186 //             // the attribute change. It shouldn't be too much trouble since most times add/remove
187 //             // will be done in the file manager (where the update can be triggered internally).
188 
189 //             gchar *local_path = g_file_get_path (file);
190 //             g_utime (local_path, NULL);
191 //             g_free (local_path);
192 //         }
193 //     }
194 }
195 
196 static void
sync_file_metadata(XAppFavorites * favorites,const gchar * uri,gboolean is_favorite)197 sync_file_metadata (XAppFavorites *favorites,
198                     const gchar   *uri,
199                     gboolean       is_favorite)
200 {
201     /* Disabled - this is less than optimal, and is implemented instead in
202      * nemo, currently. This could be changed later to help support other browsers.
203      * Also, this only works with local files. */
204     return;
205 
206     /* borrowed from nemo-vfs-file.c */
207     GFileInfo *info;
208     GFile *file;
209 
210     DEBUG ("Sync metadata: %s - Favorite? %d", uri, is_favorite);
211 
212     info = g_file_info_new ();
213 
214     if (is_favorite) {
215         g_file_info_set_attribute_string (info, FAVORITE_METADATA_KEY, META_TRUE);
216     } else {
217         /* Unset the key */
218         g_file_info_set_attribute (info, FAVORITE_METADATA_KEY, G_FILE_ATTRIBUTE_TYPE_INVALID, NULL);
219     }
220 
221     file = g_file_new_for_uri (uri);
222 
223     g_file_set_attributes_async (file,
224                                  info,
225                                  0,
226                                  G_PRIORITY_DEFAULT,
227                                  NULL,
228                                  sync_metadata_callback,
229                                  favorites);
230 
231     g_object_unref (file);
232     g_object_unref (info);
233 }
234 
235 static void
store_favorites(XAppFavorites * favorites)236 store_favorites (XAppFavorites *favorites)
237 {
238     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
239     GList *iter, *keys;
240     GPtrArray *array;
241     gchar **new_settings;
242 
243     array = g_ptr_array_new ();
244 
245     keys = g_hash_table_get_keys (priv->infos);
246 
247     for (iter = keys; iter != NULL; iter = iter->next)
248     {
249         XAppFavoriteInfo *info = (XAppFavoriteInfo *) g_hash_table_lookup (priv->infos, iter->data);
250         gchar *entry;
251 
252         entry = g_strjoin (SETTINGS_DELIMITER,
253                            info->uri,
254                            info->cached_mimetype,
255                            NULL);
256 
257         g_ptr_array_add (array, entry);
258     }
259 
260     g_ptr_array_add (array, NULL);
261 
262     g_list_free (keys);
263 
264     new_settings = (gchar **) g_ptr_array_free (array, FALSE);
265 
266     g_signal_handler_block (priv->settings, priv->settings_listener_id);
267     g_settings_set_strv (priv->settings, FAVORITES_KEY, (const gchar* const*) new_settings);
268     g_signal_handler_unblock (priv->settings, priv->settings_listener_id);
269 
270     DEBUG ("XAppFavorites: store_favorites: favorites saved");
271 
272     g_strfreev (new_settings);
273 }
274 
275 static void
load_favorites(XAppFavorites * favorites,gboolean signal_changed)276 load_favorites (XAppFavorites *favorites,
277                 gboolean       signal_changed)
278 {
279     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
280     gchar **raw_list;
281     gint i;
282 
283     if (priv->infos != NULL)
284     {
285         g_hash_table_destroy (priv->infos);
286     }
287 
288     priv->infos = g_hash_table_new_full (g_str_hash, g_str_equal,
289                                          g_free, (GDestroyNotify) xapp_favorite_info_free);
290 
291     raw_list = g_settings_get_strv (priv->settings, FAVORITES_KEY);
292 
293     if (!raw_list)
294     {
295         // no favorites
296         return;
297     }
298 
299     for (i = 0; i < g_strv_length (raw_list); i++)
300     {
301         gchar **entry = g_strsplit (raw_list[i], SETTINGS_DELIMITER, 2);
302 
303         finish_add_favorite (favorites,
304                              entry[0],  // uri
305                              entry[1],  // cached_mimetype
306                              TRUE);
307 
308         g_strfreev (entry);
309     }
310 
311     g_strfreev (raw_list);
312 
313     DEBUG ("XAppFavorites: load_favorite: favorites loaded (%d)", i);
314 
315     if (signal_changed)
316     {
317         queue_changed (favorites);
318     }
319 }
320 
321 static void
rename_favorite(XAppFavorites * favorites,const gchar * old_uri,const gchar * new_uri)322 rename_favorite (XAppFavorites *favorites,
323                  const gchar   *old_uri,
324                  const gchar   *new_uri)
325 {
326     XAppFavoriteInfo *info;
327     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
328     gchar *final_new_uri = NULL;
329 
330     if (g_str_has_prefix (old_uri, ROOT_URI))
331     {
332         // Renaming occurred inside of favorites:/// we need to identify by
333         // display name.
334 
335         const gchar *old_display_name = old_uri + strlen (ROOT_URI);
336         const gchar *new_display_name = new_uri + strlen (ROOT_URI);
337 
338         info = xapp_favorites_find_by_display_name (favorites, old_display_name);
339 
340         if (info)
341         {
342             GFile *real_file, *parent, *renamed_file;
343 
344             real_file = g_file_new_for_uri (info->uri);
345             parent = g_file_get_parent (real_file);
346 
347             renamed_file = g_file_get_child_for_display_name (parent,
348                                                               new_display_name,
349                                                               NULL);
350 
351             if (renamed_file != NULL)
352             {
353                 final_new_uri = g_file_get_uri (renamed_file);
354             }
355 
356             g_object_unref (real_file);
357             g_object_unref (parent);
358             g_clear_object (&renamed_file);
359         }
360     }
361     else
362     {
363         info = g_hash_table_lookup (priv->infos, old_uri);
364         final_new_uri = g_strdup (new_uri);
365     }
366 
367     if (info != NULL && final_new_uri != NULL)
368     {
369         gchar *mimetype = g_strdup (info->cached_mimetype);
370 
371         sync_file_metadata (favorites, info->uri, FALSE);
372 
373         g_hash_table_remove (priv->infos,
374                              (gconstpointer) info->uri);
375 
376         finish_add_favorite (favorites,
377                              final_new_uri,
378                              mimetype,
379                              FALSE);
380 
381         sync_file_metadata (favorites, final_new_uri, TRUE);
382 
383         g_free (mimetype);
384     }
385 
386     g_free (final_new_uri);
387 }
388 
389 static void
remove_favorite(XAppFavorites * favorites,const gchar * uri)390 remove_favorite (XAppFavorites *favorites,
391                  const gchar   *uri)
392 {
393     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
394     gchar *real_uri;
395 
396     if (g_str_has_prefix (uri, "favorites"))
397     {
398         GFile *file = g_file_new_for_uri (uri);
399         real_uri = favorite_vfs_file_get_real_uri (file);
400 
401         g_object_unref (file);
402     }
403     else
404     {
405         real_uri = g_strdup (uri);
406     }
407 
408     g_return_if_fail (real_uri != NULL);
409 
410     DEBUG ("XAppFavorites: remove favorite: %s", real_uri);
411 
412     // It may be orphaned for some reason.. even if it's not in gsettings, still try
413     // to remove the favorite attribute.
414     sync_file_metadata (favorites, real_uri, FALSE);
415 
416     if (!g_hash_table_remove (priv->infos, real_uri))
417     {
418         DEBUG ("XAppFavorites: remove_favorite: could not find favorite for uri '%s'", real_uri);
419         g_free (real_uri);
420         return;
421     }
422 
423     g_free (real_uri);
424 
425     store_favorites (favorites);
426     queue_changed (favorites);
427 }
428 
429 static void
deduplicate_display_names(XAppFavorites * favorites,GHashTable * infos)430 deduplicate_display_names (XAppFavorites *favorites,
431                            GHashTable    *infos)
432 {
433     GList *fav_uris, *ptr;
434     GHashTable *lists_of_keys_by_basename = g_hash_table_new_full (g_str_hash, g_str_equal,
435                                                                    g_free, NULL);
436     GHashTableIter iter;
437 
438     fav_uris = g_hash_table_get_keys (infos);
439 
440     for (ptr = fav_uris; ptr != NULL; ptr = ptr->next)
441     {
442         GList *uris;
443         const gchar *uri = (gchar *) ptr->data;
444         gchar *original_display_name = g_path_get_basename (uri);
445 
446         if (g_hash_table_contains (lists_of_keys_by_basename, original_display_name))
447         {
448             uris = g_hash_table_lookup (lists_of_keys_by_basename, original_display_name);
449 
450             // this could be prepend, but then the value in the table would have to be replaced
451             uris = g_list_append ((GList *) uris, g_strdup (uri));
452         }
453         else
454         {
455             uris = g_list_prepend (NULL, g_strdup (uri));
456             g_hash_table_insert (lists_of_keys_by_basename,
457                                  g_strdup (original_display_name),
458                                  uris);
459         }
460 
461         g_free (original_display_name);
462     }
463 
464     g_list_free (fav_uris);
465 
466     gpointer key, value;
467 
468     g_hash_table_iter_init (&iter, lists_of_keys_by_basename);
469 
470     while (g_hash_table_iter_next (&iter, &key, &value))
471     {
472         GList *same_names_list, *uri_ptr;
473         gchar *common_display_name = NULL;
474 
475         if (((GList *) value)->next == NULL)
476         {
477             // Single member of current common name list;
478             g_list_free_full ((GList *) value, g_free);
479             continue;
480         }
481         // Now we know we have a list of uris that would have identical display names
482         // Add a part of the uri after each to distinguish them.
483         common_display_name = g_uri_unescape_string ((const gchar *) key, NULL);
484         same_names_list = (GList *) value;
485 
486         for (uri_ptr = same_names_list; uri_ptr != NULL; uri_ptr = uri_ptr->next)
487         {
488             XAppFavoriteInfo *info;
489             GFile *uri_file, *home_file, *parent_file;
490             GString *new_display_string;
491             const gchar *current_uri;
492 
493             current_uri = (const gchar *) uri_ptr->data;
494 
495             uri_file = g_file_new_for_uri (current_uri);
496             parent_file = g_file_get_parent (uri_file);
497             home_file = g_file_new_for_path (g_get_home_dir());
498 
499             new_display_string = g_string_new (common_display_name);
500             g_string_append (new_display_string, "  (");
501 
502             // How much effort should we put into duplicate naming? Keeping it
503             // simple like this won't work all the time.
504             gchar *parent_basename = g_file_get_basename (parent_file);
505             g_string_append (new_display_string, parent_basename);
506             g_free (parent_basename);
507 
508             // TODO: ellipsized deduplication paths?
509 
510             // if (g_file_has_prefix (parent_file, home_file))
511             // {
512             //     gchar *home_rpath = g_file_get_relative_path (home_file, parent_file);
513             //     gchar *home_basename = g_file_get_basename (home_file);
514 
515             //     if (strlen (home_rpath) < MAX_DISPLAY_URI_LENGTH)
516             //     {
517             //         g_string_append (new_display_string, home_basename);
518             //         g_string_append (new_display_string, "/");
519             //         g_string_append (new_display_string, home_rpath);
520             //     }
521             //     else
522             //     {
523             //         gchar *parent_basename = g_file_get_basename (parent_file);
524 
525             //         g_string_append (new_display_string, home_basename);
526             //         g_string_append (new_display_string, "/.../");
527             //         g_string_append (new_display_string, parent_basename);
528 
529             //         g_free (parent_basename);
530             //     }
531 
532             //     g_free (home_rpath);
533             //     g_free (home_basename);
534             // }
535             // else
536             // {
537             //     GString *tmp_string = g_string_new (NULL);
538 
539             //     if (g_file_is_native (parent_file))
540             //     {
541             //         g_string_append (tmp_string, g_file_peek_path (parent_file));
542             //     }
543             //     else
544             //     {
545             //         g_string_append (tmp_string, current_uri);
546             //     }
547 
548             //     if (tmp_string->len > MAX_DISPLAY_URI_LENGTH)
549             //     {
550             //         gint diff;
551             //         gint replace_pos;
552 
553             //         diff = tmp_string->len - MAX_DISPLAY_URI_LENGTH;
554             //         replace_pos = (tmp_string->len / 2) - (diff / 2) - 2;
555 
556             //         g_string_erase (tmp_string,
557             //                         replace_pos,
558             //                         diff);
559             //         g_string_insert (tmp_string,
560             //                          replace_pos,
561             //                          "...");
562             //     }
563 
564             //     g_string_append (new_display_string, tmp_string->str);
565             //     g_string_free (tmp_string, TRUE);
566             // }
567 
568             g_object_unref (uri_file);
569             g_object_unref (home_file);
570             g_object_unref (parent_file);
571 
572             g_string_append (new_display_string, ")");
573 
574             // Look up the info from our master table
575             info = g_hash_table_lookup (infos, current_uri);
576             g_free (info->display_name);
577 
578             info->display_name = g_string_free (new_display_string, FALSE);
579         }
580 
581         g_free (common_display_name);
582         g_list_free_full (same_names_list, g_free);
583     }
584 
585     // We freed the individual lists just above, only the keys will need
586     // freed here.
587     g_hash_table_destroy (lists_of_keys_by_basename);
588 }
589 
590 static void
finish_add_favorite(XAppFavorites * favorites,const gchar * uri,const gchar * cached_mimetype,gboolean from_saved)591 finish_add_favorite (XAppFavorites *favorites,
592                      const gchar   *uri,
593                      const gchar   *cached_mimetype,
594                      gboolean       from_saved)
595 {
596     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
597     XAppFavoriteInfo *info;
598     gchar *unescaped_uri;
599 
600     // Check if it's there again, in case it was added while we were getting mimetype.
601     if (g_hash_table_contains (priv->infos, uri))
602     {
603         DEBUG ("XAppFavorites: favorite for '%s' exists, ignoring", uri);
604         return;
605     }
606 
607     info = g_slice_new0 (XAppFavoriteInfo);
608     info->uri = g_strdup (uri);
609 
610     unescaped_uri = g_uri_unescape_string (uri, NULL);
611     info->display_name = g_path_get_basename (unescaped_uri);
612     g_free (unescaped_uri);
613 
614     info->cached_mimetype = g_strdup (cached_mimetype);
615 
616     g_hash_table_insert (priv->infos, (gpointer) g_strdup (uri), (gpointer) info);
617 
618     DEBUG ("XAppFavorites: added favorite: %s", uri);
619 
620     deduplicate_display_names (favorites, priv->infos);
621 
622     if (from_saved)
623     {
624         return;
625     }
626 
627     store_favorites (favorites);
628     queue_changed (favorites);
629 }
630 
631 static void
on_content_type_info_received(GObject * source,GAsyncResult * res,gpointer user_data)632 on_content_type_info_received (GObject      *source,
633                                GAsyncResult *res,
634                                gpointer      user_data)
635 {
636     XAppFavorites *favorites = XAPP_FAVORITES (user_data);
637     GFile *file;
638     GFileInfo *file_info;
639     GError *error;
640     gchar *cached_mimetype, *uri;
641 
642     file = G_FILE (source);
643     uri = g_file_get_uri (file);
644     error = NULL;
645     cached_mimetype = NULL;
646 
647     file_info = g_file_query_info_finish (file, res, &error);
648 
649     if (error)
650     {
651         DEBUG ("XAppFavorites: problem trying to figure out content type for uri '%s': %s",
652                  uri, error->message);
653         g_error_free (error);
654     }
655 
656     if (file_info)
657     {
658         cached_mimetype = g_strdup (g_file_info_get_content_type (file_info));
659 
660         if (cached_mimetype == NULL)
661         {
662             cached_mimetype = g_strdup ("application/unknown");
663         }
664 
665         finish_add_favorite (favorites,
666                              uri,
667                              cached_mimetype,
668                              FALSE);
669 
670         sync_file_metadata (favorites, uri, TRUE);
671     }
672 
673     g_free (uri);
674     g_free (cached_mimetype);
675     g_clear_object (&file_info);
676     g_object_unref (file);
677 }
678 
679 static void
add_favorite(XAppFavorites * favorites,const gchar * uri)680 add_favorite (XAppFavorites *favorites,
681               const gchar   *uri)
682 {
683     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
684     GFile *file;
685 
686     if (g_hash_table_contains (priv->infos, uri))
687     {
688         DEBUG ("XAppFavorites: favorite for '%s' exists, ignoring", uri);
689         return;
690     }
691 
692     file = g_file_new_for_uri (uri);
693 
694     g_file_query_info_async (file,
695                              G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
696                              G_FILE_QUERY_INFO_NONE,
697                              G_PRIORITY_LOW,
698                              NULL,
699                              on_content_type_info_received,
700                              favorites);
701 }
702 
703 static void
on_settings_list_changed(GSettings * settings,gchar * key,gpointer user_data)704 on_settings_list_changed (GSettings *settings,
705                           gchar     *key,
706                           gpointer   user_data)
707 {
708     XAppFavorites *favorites = XAPP_FAVORITES (user_data);
709 
710     load_favorites (favorites, TRUE);
711 }
712 
713 static void
xapp_favorites_init(XAppFavorites * favorites)714 xapp_favorites_init (XAppFavorites *favorites)
715 {
716     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
717 
718     DEBUG ("XAppFavorites: init:");
719 
720     priv->settings = g_settings_new (FAVORITES_SCHEMA);
721     priv->settings_listener_id = g_signal_connect (priv->settings,
722                                                    "changed::" FAVORITES_KEY,
723                                                    G_CALLBACK (on_settings_list_changed),
724                                                    favorites);
725 
726     load_favorites (favorites, FALSE);
727 }
728 
729 static void
xapp_favorites_dispose(GObject * object)730 xapp_favorites_dispose (GObject *object)
731 {
732     XAppFavorites *favorites = XAPP_FAVORITES (object);
733     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
734 
735     DEBUG ("XAppFavorites dispose (%p)", object);
736 
737     g_clear_object (&priv->settings);
738     g_clear_pointer (&priv->infos, g_hash_table_destroy);
739 
740     G_OBJECT_CLASS (xapp_favorites_parent_class)->dispose (object);
741 }
742 
743 static void
xapp_favorites_finalize(GObject * object)744 xapp_favorites_finalize (GObject *object)
745 {
746     DEBUG ("XAppFavorites finalize (%p)", object);
747 
748     G_OBJECT_CLASS (xapp_favorites_parent_class)->finalize (object);
749 }
750 
751 static void
xapp_favorites_class_init(XAppFavoritesClass * klass)752 xapp_favorites_class_init (XAppFavoritesClass *klass)
753 {
754     GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
755 
756     gobject_class->dispose = xapp_favorites_dispose;
757     gobject_class->finalize = xapp_favorites_finalize;
758 
759     /**
760      * XAppFavorites::changed:
761 
762      * Notifies when the favorites list has changed.
763      */
764     signals [CHANGED] =
765         g_signal_new ("changed",
766                       XAPP_TYPE_FAVORITES,
767                       G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
768                       0,
769                       NULL, NULL, NULL,
770                       G_TYPE_NONE, 0);
771 }
772 
773 /**
774  * xapp_favorites_get_default:
775  *
776  * Returns the #XAppFavorites instance.
777  *
778  * Returns: (transfer none): the XAppFavorites instance for the process. Do not free.
779  *
780  * Since: 2.0
781  */
782 XAppFavorites *
xapp_favorites_get_default(void)783 xapp_favorites_get_default (void)
784 {
785     if (global_favorites == NULL)
786     {
787         init_favorite_vfs ();
788         global_favorites = g_object_new (XAPP_TYPE_FAVORITES, NULL);
789     }
790 
791     return global_favorites;
792 }
793 
794 typedef struct {
795     GList *items;
796     const gchar **mimetypes;
797 } MatchData;
798 
799 void
match_mimetypes(gpointer key,gpointer value,gpointer user_data)800 match_mimetypes (gpointer key,
801                  gpointer value,
802                  gpointer user_data)
803 {
804     MatchData *data = (MatchData *) user_data;
805     const XAppFavoriteInfo *info = (XAppFavoriteInfo *) value;
806 
807     if (data->mimetypes == NULL)
808     {
809         data->items = g_list_prepend (data->items, xapp_favorite_info_copy (info));
810         return;
811     }
812 
813     gint i;
814 
815     for (i = 0; i < g_strv_length ((gchar **) data->mimetypes); i++)
816     {
817         if (g_content_type_is_mime_type (info->cached_mimetype, data->mimetypes[i]))
818         {
819             data->items = g_list_prepend (data->items, xapp_favorite_info_copy (info));
820             return;
821         }
822     }
823 }
824 
825 /**
826  * xapp_favorites_get_favorites:
827  * @favorites: The #XAppFavorites
828  * @mimetypes: (nullable) (array zero-terminated=1): The mimetypes to filter by for results
829  *
830  * Gets a list of all favorites.  If mimetype is not %NULL, the list will
831  * contain only favorites with that mimetype.
832  *
833  * Returns: (element-type XAppFavoriteInfo) (transfer full): a list of #XAppFavoriteInfos.
834             Free the list with #g_list_free, free elements with #xapp_favorite_info_free.
835  *
836  * Since: 2.0
837  */
838 GList *
xapp_favorites_get_favorites(XAppFavorites * favorites,const gchar * const * mimetypes)839 xapp_favorites_get_favorites (XAppFavorites       *favorites,
840                               const gchar * const *mimetypes)
841 {
842     g_return_val_if_fail (XAPP_IS_FAVORITES (favorites), NULL);
843     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
844     GList *ret = NULL;
845     MatchData data;
846 
847     data.items = NULL;
848     data.mimetypes = (const gchar **) mimetypes;
849     g_hash_table_foreach (priv->infos,
850                           (GHFunc) match_mimetypes,
851                           &data);
852 
853     ret = g_list_reverse (data.items);
854 
855     gchar *typestring = mimetypes ? g_strjoinv (", ", (gchar **) mimetypes) : NULL;
856     DEBUG ("XAppFavorites: get_favorites returning list for mimetype '%s' (%d items)",
857              typestring, g_list_length (ret));
858     g_free (typestring);
859 
860     return ret;
861 }
862 
863 /**
864  * xapp_favorites_get_n_favorites:
865  * @favorites: The #XAppFavorites
866  *
867  * Returns: The number of favorite files
868 
869  * Since: 2.0
870  */
871 gint
xapp_favorites_get_n_favorites(XAppFavorites * favorites)872 xapp_favorites_get_n_favorites (XAppFavorites *favorites)
873 {
874     g_return_val_if_fail (XAPP_IS_FAVORITES (favorites), 0);
875     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
876     gint n;
877 
878     n = g_hash_table_size (priv->infos);
879 
880     DEBUG ("XAppFavorites: get_n_favorites returning number of items: %d.", n);
881 
882     return n;
883 }
884 
885 static gboolean
lookup_display_name(gpointer key,gpointer value,gpointer user_data)886 lookup_display_name (gpointer key,
887                      gpointer value,
888                      gpointer user_data)
889 {
890     XAppFavoriteInfo *info = (XAppFavoriteInfo *) value;
891 
892     if (g_strcmp0 (info->display_name, (const gchar *) user_data) == 0)
893     {
894         return TRUE;
895     }
896 
897     return FALSE;
898 }
899 
900 /**
901  * xapp_favorites_find_by_display_name:
902  * @favorites: The #XAppFavorites
903  * @display_name: (not nullable): The display name to lookup info for.
904  *
905  * Looks for an XAppFavoriteInfo that corresponds to @display_name.
906  *
907  * Returns: (transfer none): an XAppFavoriteInfo or NULL if one was not found. This is owned
908  *          by the favorites manager and should not be freed.
909  *
910  * Since: 2.0
911  */
912 XAppFavoriteInfo *
xapp_favorites_find_by_display_name(XAppFavorites * favorites,const gchar * display_name)913 xapp_favorites_find_by_display_name (XAppFavorites *favorites,
914                                      const gchar   *display_name)
915 {
916     g_return_val_if_fail (XAPP_IS_FAVORITES (favorites), NULL);
917     g_return_val_if_fail (display_name != NULL, NULL);
918 
919     XAppFavoriteInfo *info;
920     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
921 
922     info = g_hash_table_find (priv->infos,
923                               (GHRFunc) lookup_display_name,
924                               (gpointer) display_name);
925 
926     if (info != NULL)
927     {
928         return info;
929     }
930 
931     return NULL;
932 }
933 
934 /**
935  * xapp_favorites_find_by_uri:
936  * @favorites: The #XAppFavorites
937  * @uri: (not nullable): The uri to lookup info for.
938  *
939  * Looks for an XAppFavoriteInfo that corresponds to @uri.
940  *
941  * Returns: (transfer none): an XAppFavoriteInfo or NULL if one was not found. This is owned
942  *          by the favorites manager and should not be freed.
943  *
944  * Since: 2.0
945  */
946 XAppFavoriteInfo *
xapp_favorites_find_by_uri(XAppFavorites * favorites,const gchar * uri)947 xapp_favorites_find_by_uri (XAppFavorites *favorites,
948                             const gchar   *uri)
949 {
950     g_return_val_if_fail (XAPP_IS_FAVORITES (favorites), NULL);
951     g_return_val_if_fail (uri != NULL, NULL);
952 
953     XAppFavoriteInfo *info;
954     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
955 
956     info = g_hash_table_lookup (priv->infos, uri);
957 
958     if (info != NULL)
959     {
960         return (XAppFavoriteInfo *) info;
961     }
962 
963     return NULL;
964 }
965 
966 /**
967  * xapp_favorites_add:
968  * @favorites: The #XAppFavorites
969  * @uri: The uri the favorite is for
970  *
971  * Adds a new favorite.  If the uri already exists, this does nothing.
972  *
973  * Since: 2.0
974  */
975 void
xapp_favorites_add(XAppFavorites * favorites,const gchar * uri)976 xapp_favorites_add (XAppFavorites *favorites,
977                     const gchar   *uri)
978 {
979     g_return_if_fail (XAPP_IS_FAVORITES (favorites));
980     g_return_if_fail (uri != NULL);
981 
982     add_favorite (favorites, uri);
983 }
984 
985 /**
986  * xapp_favorites_remove:
987  * @favorites: The #XAppFavorites
988  * @uri: The uri for the favorite being removed
989  *
990  * Removes a favorite from the list.
991  *
992  * Since: 2.0
993  */
994 void
xapp_favorites_remove(XAppFavorites * favorites,const gchar * uri)995 xapp_favorites_remove (XAppFavorites *favorites,
996                        const gchar   *uri)
997 {
998     g_return_if_fail (XAPP_IS_FAVORITES (favorites));
999     g_return_if_fail (uri != NULL);
1000 
1001     remove_favorite (favorites, uri);
1002 }
1003 
1004 static void
launch_uri_callback(GObject * source,GAsyncResult * res,gpointer user_data)1005 launch_uri_callback (GObject      *source,
1006                      GAsyncResult *res,
1007                      gpointer      user_data)
1008 {
1009     gchar *uri = (gchar *) user_data;
1010     GError *error;
1011 
1012     error = NULL;
1013 
1014     if (!g_app_info_launch_default_for_uri_finish (res, &error))
1015     {
1016         if (error)
1017         {
1018             DEBUG ("XAppFavorites: launch: error opening uri '%s': %s", uri, error->message);
1019             g_error_free (error);
1020         }
1021     }
1022 
1023     g_free (uri);
1024 }
1025 
1026 /**
1027  * xapp_favorites_launch:
1028  * @favorites: The #XAppFavorites
1029  * @uri: The uri for the favorite to launch
1030  * @timestamp: The timestamp from an event or 0
1031  *
1032  * Opens a favorite in its default app.
1033  *
1034  * Since: 2.0
1035  */
1036 void
xapp_favorites_launch(XAppFavorites * favorites,const gchar * uri,guint32 timestamp)1037 xapp_favorites_launch (XAppFavorites *favorites,
1038                        const gchar   *uri,
1039                        guint32        timestamp)
1040 {
1041     GdkDisplay *display;
1042     GdkAppLaunchContext *launch_context;
1043 
1044     display = gdk_display_get_default ();
1045     launch_context = gdk_display_get_app_launch_context (display);
1046     gdk_app_launch_context_set_timestamp (launch_context, timestamp);
1047 
1048     g_app_info_launch_default_for_uri_async (uri,
1049                                              G_APP_LAUNCH_CONTEXT (launch_context),
1050                                              NULL,
1051                                              launch_uri_callback,
1052                                              g_strdup (uri));
1053 
1054     g_object_unref (launch_context);
1055 }
1056 
1057 /**
1058  * xapp_favorites_rename:
1059  * @old_uri: the old favorite's uri.
1060  * @new_uri: The new uri.
1061  *
1062  * Removes old_uri and adds new_uri. This is mainly for file managers to use as
1063  * a convenience instead of add/remove, and guarantees the result, without having to
1064  * worry about multiple dbus calls (gsettings).
1065  *
1066  * Since: 2.0
1067  */
1068 void
xapp_favorites_rename(XAppFavorites * favorites,const gchar * old_uri,const gchar * new_uri)1069 xapp_favorites_rename (XAppFavorites *favorites,
1070                        const gchar   *old_uri,
1071                        const gchar   *new_uri)
1072 {
1073     g_return_if_fail (XAPP_IS_FAVORITES (favorites));
1074     g_return_if_fail (old_uri != NULL && new_uri != NULL);
1075 
1076     rename_favorite (favorites, old_uri, new_uri);
1077 }
1078 
1079 typedef struct {
1080     XAppFavorites *favorites;
1081     guint update_id;
1082 
1083     GDestroyNotify destroy_func;
1084     gpointer user_data;
1085 } DestroyData;
1086 
1087 typedef struct {
1088     XAppFavorites *favorites;
1089     XAppFavoritesItemSelectedCallback callback;
1090     gchar *uri;
1091     gpointer user_data;
1092 } ItemCallbackData;
1093 
1094 static void
cb_data_destroy_notify(gpointer callback_data,GObject * object)1095 cb_data_destroy_notify (gpointer callback_data,
1096                         GObject *object)
1097 {
1098     DestroyData *dd = (DestroyData *) callback_data;
1099 
1100     if (dd->update_id > 0)
1101     {
1102         g_signal_handler_disconnect (dd->favorites, dd->update_id);
1103     }
1104 
1105     dd->destroy_func (dd->user_data);
1106 
1107     g_slice_free (DestroyData, dd);
1108 }
1109 
1110 static void
free_item_callback_data(gpointer callback_data,GClosure * closure)1111 free_item_callback_data (gpointer  callback_data,
1112                          GClosure *closure)
1113 {
1114     ItemCallbackData *data = (ItemCallbackData *) callback_data;
1115     g_free (data->uri);
1116     g_slice_free (ItemCallbackData, data);
1117 }
1118 
1119 static void
item_activated(GObject * item,gpointer user_data)1120 item_activated (GObject *item,
1121                 gpointer user_data)
1122 {
1123     ItemCallbackData *data = (ItemCallbackData *) user_data;
1124 
1125     data->callback (data->favorites,
1126                     data->uri,
1127                     data->user_data);
1128 }
1129 
1130 static void
remove_menu_item(GtkWidget * item,gpointer user_data)1131 remove_menu_item (GtkWidget *item,
1132                   gpointer   user_data)
1133 {
1134     gtk_container_remove (GTK_CONTAINER (user_data), item);
1135 }
1136 
1137 static void
populate_menu(XAppFavorites * favorites,GtkMenu * menu)1138 populate_menu (XAppFavorites *favorites,
1139                GtkMenu       *menu)
1140 {
1141     GList *fav_list, *ptr;
1142     GtkWidget *item;
1143     XAppFavoritesItemSelectedCallback callback;
1144     gpointer user_data;
1145     const gchar **mimetypes;
1146 
1147     gtk_container_foreach (GTK_CONTAINER (menu), (GtkCallback) remove_menu_item, menu);
1148 
1149     mimetypes = (const gchar **) g_object_get_data (G_OBJECT (menu), "mimetypes");
1150     callback = g_object_get_data (G_OBJECT (menu), "activate-cb");
1151     user_data = g_object_get_data (G_OBJECT (menu), "user-data");
1152 
1153     fav_list = xapp_favorites_get_favorites (favorites, mimetypes);
1154 
1155     if (fav_list == NULL)
1156     {
1157         return;
1158     }
1159 
1160     for (ptr = fav_list; ptr != NULL; ptr = ptr->next)
1161     {
1162         XAppFavoriteInfo *info = (XAppFavoriteInfo *) ptr->data;
1163         ItemCallbackData *data;
1164 
1165         if (mimetypes != NULL)
1166         {
1167             item = gtk_menu_item_new_with_label (info->display_name);
1168         }
1169         else
1170         {
1171             GtkWidget *image;
1172             GIcon *icon;
1173 
1174             icon = g_content_type_get_symbolic_icon (info->cached_mimetype);
1175             image = gtk_image_new_from_gicon (icon, GTK_ICON_SIZE_MENU);
1176             g_object_unref (icon);
1177 
1178             item = gtk_image_menu_item_new_with_label (info->display_name);
1179             gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
1180         }
1181 
1182         data = g_slice_new0 (ItemCallbackData);
1183         data->favorites = favorites;
1184         data->uri = g_strdup (info->uri);
1185         data->callback = callback;
1186         data->user_data = user_data;
1187 
1188         gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
1189 
1190         g_signal_connect_data (item,
1191                                "activate", G_CALLBACK (item_activated),
1192                                data, (GClosureNotify) free_item_callback_data, 0);
1193     }
1194 
1195     g_list_free_full (fav_list, (GDestroyNotify) xapp_favorite_info_free);
1196 
1197     gtk_widget_show_all (GTK_WIDGET (menu));
1198 }
1199 
1200 static void
refresh_menu_items(XAppFavorites * favorites,gpointer user_data)1201 refresh_menu_items (XAppFavorites *favorites,
1202                     gpointer       user_data)
1203 {
1204     g_return_if_fail (XAPP_IS_FAVORITES (favorites));
1205     g_return_if_fail (GTK_IS_MENU (user_data));
1206 
1207     GtkMenu *menu = GTK_MENU (user_data);
1208 
1209     populate_menu (favorites, menu);
1210 }
1211 
1212 /**
1213  * xapp_favorites_create_menu:
1214  * @favorites: The #XAppFavorites instance.
1215  * @mimetypes: (nullable): The mimetypes to filter for, or NULL to include all favorites.
1216  * @callback: (scope notified): (closure user_data): The callback to use when a menu item has been selected.
1217  * @user_data: (closure): The data to pass to the callback
1218  * @func: Destroy function for user_data
1219  *
1220  * Generates a GtkMenu widget populated with favorites. The callback will be called when
1221  * a menu item has been activated, and will include the uri of the respective item.
1222  *
1223  * Returns: (transfer full): a new #GtkMenu populated with a list of favorites, or NULL
1224             if there are no favorites.
1225  *
1226  * Since: 2.0
1227  */
1228 GtkWidget *
xapp_favorites_create_menu(XAppFavorites * favorites,const gchar ** mimetypes,XAppFavoritesItemSelectedCallback callback,gpointer user_data,GDestroyNotify func)1229 xapp_favorites_create_menu (XAppFavorites                      *favorites,
1230                             const gchar                       **mimetypes,
1231                             XAppFavoritesItemSelectedCallback   callback,
1232                             gpointer                            user_data,
1233                             GDestroyNotify                      func)
1234 {
1235     g_return_val_if_fail (XAPP_IS_FAVORITES (favorites), NULL);
1236     GtkWidget *menu;
1237 
1238     menu = gtk_menu_new ();
1239 
1240     g_object_set_data_full (G_OBJECT (menu),
1241                             "mimetype", g_strdupv ((gchar **) mimetypes),
1242                             (GDestroyNotify) g_strfreev);
1243 
1244     g_object_set_data (G_OBJECT (menu),
1245                        "activate-cb", callback);
1246 
1247     g_object_set_data (G_OBJECT (menu),
1248                        "user-data", user_data);
1249 
1250     populate_menu (favorites, GTK_MENU (menu));
1251 
1252     DestroyData *dd = g_slice_new0 (DestroyData);
1253     dd->destroy_func = func;
1254     dd->user_data = user_data;
1255     dd->favorites = favorites;
1256     dd->update_id = g_signal_connect (favorites,
1257                                       "changed",
1258                                       G_CALLBACK (refresh_menu_items),
1259                                       menu);
1260 
1261     g_object_weak_ref (G_OBJECT (menu), (GWeakNotify) cb_data_destroy_notify, dd);
1262 
1263     return menu;
1264 }
1265 
1266 static GList *
populate_action_list(XAppFavorites * favorites,const gchar ** mimetypes)1267 populate_action_list (XAppFavorites  *favorites,
1268                       const gchar   **mimetypes)
1269 {
1270     GList *fav_list, *ptr;
1271     GList *actions;
1272     GtkAction *action;
1273     gint i;
1274 
1275     fav_list = xapp_favorites_get_favorites (favorites, mimetypes);
1276 
1277     if (fav_list == NULL)
1278     {
1279         return NULL;
1280     }
1281 
1282     actions = NULL;
1283 
1284     for (ptr = fav_list, i = 0; ptr != NULL; ptr = ptr->next, i++)
1285     {
1286         XAppFavoriteInfo *info = (XAppFavoriteInfo *) ptr->data;
1287 
1288         if (mimetypes != NULL)
1289         {
1290             action = g_object_new (GTK_TYPE_ACTION,
1291                                    "name", info->uri,
1292                                    "label", info->display_name,
1293                                    NULL);
1294         }
1295         else
1296         {
1297             GIcon *icon;
1298             icon = g_content_type_get_symbolic_icon (info->cached_mimetype);
1299 
1300             action = g_object_new (GTK_TYPE_ACTION,
1301                                    "name", info->uri,
1302                                    "label", info->display_name,
1303                                    "gicon", icon,
1304                                    NULL);
1305 
1306             g_free (icon);
1307         }
1308 
1309         actions = g_list_prepend (actions, action);
1310     }
1311 
1312     actions = g_list_reverse (actions);
1313 
1314     return actions;
1315 }
1316 
1317 /**
1318  * xapp_favorites_create_actions:
1319  * @favorites: The #XAppFavorites instance.
1320  * @mimetypes: (nullable): The mimetypes to filter for, or NULL to include all favorites.
1321  *
1322  * Generates a list of favorite GtkActions.
1323  *
1324  * Returns: (element-type Gtk.Action) (transfer full): a new #GtkActionGroup populated with a list of favorites, or NULL
1325             if there are no favorites.
1326 
1327  * Since: 2.0
1328  */
1329 GList *
xapp_favorites_create_actions(XAppFavorites * favorites,const gchar ** mimetypes)1330 xapp_favorites_create_actions (XAppFavorites  *favorites,
1331                                const gchar   **mimetypes)
1332 {
1333     g_return_val_if_fail (XAPP_IS_FAVORITES (favorites), NULL);
1334     GList *actions;
1335 
1336     actions = populate_action_list (favorites,
1337                                     mimetypes);
1338 
1339     return actions;
1340 }
1341 
1342 /* Used by favorite_vfs_file */
1343 GList *
_xapp_favorites_get_display_names(XAppFavorites * favorites)1344 _xapp_favorites_get_display_names (XAppFavorites *favorites)
1345 {
1346     g_return_val_if_fail (XAPP_IS_FAVORITES (favorites), NULL);
1347     XAppFavoritesPrivate *priv = xapp_favorites_get_instance_private (favorites);
1348     GHashTableIter iter;
1349     GList *ret;
1350     gpointer key, value;
1351 
1352     ret = NULL;
1353     g_hash_table_iter_init (&iter, priv->infos);
1354 
1355     while (g_hash_table_iter_next (&iter, &key, &value))
1356     {
1357         XAppFavoriteInfo *info = (XAppFavoriteInfo *) value;
1358         ret = g_list_prepend (ret, info->display_name);
1359     }
1360 
1361     ret = g_list_reverse (ret);
1362     return ret;
1363 }
1364 
1365