/*
* nautilus-shell-search-provider.c - Implementation of a GNOME Shell
* search provider
*
* Copyright (C) 2012 Red Hat, Inc.
*
* Nautilus is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* Nautilus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*
* Authors: Cosimo Cecchi
*
*/
#include
#include
#include
#include
#include
#include "nautilus-file.h"
#include "nautilus-file-utilities.h"
#include "nautilus-search-engine.h"
#include "nautilus-search-provider.h"
#include "nautilus-ui-utilities.h"
#include "nautilus-application.h"
#include "nautilus-bookmark-list.h"
#include "nautilus-shell-search-provider-generated.h"
#include "nautilus-shell-search-provider.h"
typedef struct
{
NautilusShellSearchProvider *self;
NautilusSearchEngine *engine;
NautilusQuery *query;
GHashTable *hits;
GDBusMethodInvocation *invocation;
gint64 start_time;
} PendingSearch;
struct _NautilusShellSearchProvider
{
GObject parent;
NautilusShellSearchProvider2 *skeleton;
PendingSearch *current_search;
GHashTable *metas_cache;
};
G_DEFINE_TYPE (NautilusShellSearchProvider, nautilus_shell_search_provider, G_TYPE_OBJECT)
static gchar *
get_display_name (NautilusShellSearchProvider *self,
NautilusFile *file)
{
GFile *location;
NautilusBookmark *bookmark;
NautilusBookmarkList *bookmarks;
bookmarks = nautilus_application_get_bookmarks (NAUTILUS_APPLICATION (g_application_get_default ()));
location = nautilus_file_get_location (file);
bookmark = nautilus_bookmark_list_item_with_location (bookmarks, location, NULL);
g_object_unref (location);
if (bookmark)
{
return g_strdup (nautilus_bookmark_get_name (bookmark));
}
else
{
return nautilus_file_get_display_name (file);
}
}
static GIcon *
get_gicon (NautilusShellSearchProvider *self,
NautilusFile *file)
{
GFile *location;
NautilusBookmark *bookmark;
NautilusBookmarkList *bookmarks;
bookmarks = nautilus_application_get_bookmarks (NAUTILUS_APPLICATION (g_application_get_default ()));
location = nautilus_file_get_location (file);
bookmark = nautilus_bookmark_list_item_with_location (bookmarks, location, NULL);
g_object_unref (location);
if (bookmark)
{
return nautilus_bookmark_get_icon (bookmark);
}
else
{
return nautilus_file_get_gicon (file, 0);
}
}
static void
pending_search_free (PendingSearch *search)
{
g_hash_table_destroy (search->hits);
g_clear_object (&search->query);
g_clear_object (&search->engine);
g_clear_object (&search->invocation);
g_slice_free (PendingSearch, search);
}
static void
pending_search_finish (PendingSearch *search,
GDBusMethodInvocation *invocation,
GVariant *result)
{
NautilusShellSearchProvider *self = search->self;
g_dbus_method_invocation_return_value (invocation, result);
if (search == self->current_search)
{
self->current_search = NULL;
}
g_application_release (g_application_get_default ());
pending_search_free (search);
}
static void
cancel_current_search (NautilusShellSearchProvider *self)
{
if (self->current_search != NULL)
{
nautilus_search_provider_stop (NAUTILUS_SEARCH_PROVIDER (self->current_search->engine));
}
}
static void
search_hits_added_cb (NautilusSearchEngine *engine,
GList *hits,
gpointer user_data)
{
PendingSearch *search = user_data;
GList *l;
NautilusSearchHit *hit;
const gchar *hit_uri;
g_debug ("*** Search engine hits added");
for (l = hits; l != NULL; l = l->next)
{
hit = l->data;
nautilus_search_hit_compute_scores (hit, search->query);
hit_uri = nautilus_search_hit_get_uri (hit);
g_debug (" %s", hit_uri);
g_hash_table_replace (search->hits, g_strdup (hit_uri), g_object_ref (hit));
}
}
static gint
search_hit_compare_relevance (gconstpointer a,
gconstpointer b)
{
NautilusSearchHit *hit_a, *hit_b;
gdouble relevance_a, relevance_b;
hit_a = NAUTILUS_SEARCH_HIT ((gpointer) a);
hit_b = NAUTILUS_SEARCH_HIT ((gpointer) b);
relevance_a = nautilus_search_hit_get_relevance (hit_a);
relevance_b = nautilus_search_hit_get_relevance (hit_b);
if (relevance_a > relevance_b)
{
return -1;
}
else if (relevance_a == relevance_b)
{
return 0;
}
return 1;
}
static void
search_finished_cb (NautilusSearchEngine *engine,
NautilusSearchProviderStatus status,
gpointer user_data)
{
PendingSearch *search = user_data;
GList *hits, *l;
NautilusSearchHit *hit;
GVariantBuilder builder;
gint64 current_time;
current_time = g_get_monotonic_time ();
g_debug ("*** Search engine search finished - time elapsed %dms",
(gint) ((current_time - search->start_time) / 1000));
hits = g_hash_table_get_values (search->hits);
hits = g_list_sort (hits, search_hit_compare_relevance);
g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
for (l = hits; l != NULL; l = l->next)
{
hit = l->data;
g_variant_builder_add (&builder, "s", nautilus_search_hit_get_uri (hit));
}
g_list_free (hits);
pending_search_finish (search, search->invocation,
g_variant_new ("(as)", &builder));
}
static void
search_error_cb (NautilusSearchEngine *engine,
const gchar *error_message,
gpointer user_data)
{
NautilusShellSearchProvider *self = user_data;
PendingSearch *search = self->current_search;
g_debug ("*** Search engine search error");
pending_search_finish (search, search->invocation,
g_variant_new ("(as)", NULL));
}
typedef struct
{
gchar *uri;
gchar *string_for_compare;
} SearchHitCandidate;
static void
search_hit_candidate_free (SearchHitCandidate *candidate)
{
g_free (candidate->uri);
g_free (candidate->string_for_compare);
g_slice_free (SearchHitCandidate, candidate);
}
static SearchHitCandidate *
search_hit_candidate_new (const gchar *uri,
const gchar *name)
{
SearchHitCandidate *candidate = g_slice_new0 (SearchHitCandidate);
candidate->uri = g_strdup (uri);
candidate->string_for_compare = g_strdup (name);
return candidate;
}
static void
search_add_volumes_and_bookmarks (PendingSearch *search)
{
NautilusSearchHit *hit;
NautilusBookmark *bookmark;
const gchar *name;
gchar *string, *uri;
gdouble match;
GList *l, *m, *drives, *volumes, *mounts, *mounts_to_check, *candidates;
GDrive *drive;
GVolume *volume;
GMount *mount;
GFile *location;
SearchHitCandidate *candidate;
NautilusBookmarkList *bookmarks;
GList *all_bookmarks;
GVolumeMonitor *volume_monitor;
bookmarks = nautilus_application_get_bookmarks (NAUTILUS_APPLICATION (g_application_get_default ()));
all_bookmarks = nautilus_bookmark_list_get_all (bookmarks);
volume_monitor = g_volume_monitor_get ();
candidates = NULL;
/* first add bookmarks */
for (l = all_bookmarks; l != NULL; l = l->next)
{
bookmark = NAUTILUS_BOOKMARK (l->data);
name = nautilus_bookmark_get_name (bookmark);
if (name == NULL)
{
continue;
}
uri = nautilus_bookmark_get_uri (bookmark);
candidate = search_hit_candidate_new (uri, name);
candidates = g_list_prepend (candidates, candidate);
g_free (uri);
}
/* home dir */
uri = nautilus_get_home_directory_uri ();
candidate = search_hit_candidate_new (uri, _("Home"));
candidates = g_list_prepend (candidates, candidate);
g_free (uri);
/* trash */
candidate = search_hit_candidate_new ("trash:///", _("Trash"));
candidates = g_list_prepend (candidates, candidate);
/* now add mounts */
mounts_to_check = NULL;
/* first check all connected drives */
drives = g_volume_monitor_get_connected_drives (volume_monitor);
for (l = drives; l != NULL; l = l->next)
{
drive = l->data;
volumes = g_drive_get_volumes (drive);
for (m = volumes; m != NULL; m = m->next)
{
volume = m->data;
mount = g_volume_get_mount (volume);
if (mount != NULL)
{
mounts_to_check = g_list_prepend (mounts_to_check, mount);
}
}
g_list_free_full (volumes, g_object_unref);
}
g_list_free_full (drives, g_object_unref);
/* then volumes that don't have a drive */
volumes = g_volume_monitor_get_volumes (volume_monitor);
for (l = volumes; l != NULL; l = l->next)
{
volume = l->data;
drive = g_volume_get_drive (volume);
if (drive == NULL)
{
mount = g_volume_get_mount (volume);
if (mount != NULL)
{
mounts_to_check = g_list_prepend (mounts_to_check, mount);
}
}
g_clear_object (&drive);
}
g_list_free_full (volumes, g_object_unref);
/* then mounts that have no volume */
mounts = g_volume_monitor_get_mounts (volume_monitor);
for (l = mounts; l != NULL; l = l->next)
{
mount = l->data;
if (g_mount_is_shadowed (mount))
{
continue;
}
volume = g_mount_get_volume (mount);
if (volume == NULL)
{
mounts_to_check = g_list_prepend (mounts_to_check, g_object_ref (mount));
}
g_clear_object (&volume);
}
g_list_free_full (mounts, g_object_unref);
/* actually add mounts to candidates */
for (l = mounts_to_check; l != NULL; l = l->next)
{
mount = l->data;
string = g_mount_get_name (mount);
if (string == NULL)
{
continue;
}
location = g_mount_get_default_location (mount);
uri = g_file_get_uri (location);
candidate = search_hit_candidate_new (uri, string);
candidates = g_list_prepend (candidates, candidate);
g_free (uri);
g_free (string);
g_object_unref (location);
}
g_list_free_full (mounts_to_check, g_object_unref);
/* now do the actual string matching */
candidates = g_list_reverse (candidates);
for (l = candidates; l != NULL; l = l->next)
{
candidate = l->data;
match = nautilus_query_matches_string (search->query,
candidate->string_for_compare);
if (match > -1)
{
hit = nautilus_search_hit_new (candidate->uri);
nautilus_search_hit_set_fts_rank (hit, match);
nautilus_search_hit_compute_scores (hit, search->query);
g_hash_table_replace (search->hits, g_strdup (candidate->uri), hit);
}
}
g_list_free_full (candidates, (GDestroyNotify) search_hit_candidate_free);
g_object_unref (volume_monitor);
}
static NautilusQuery *
shell_query_new (gchar **terms)
{
NautilusQuery *query;
g_autoptr (GFile) home = NULL;
g_autofree gchar *terms_joined = NULL;
terms_joined = g_strjoinv (" ", terms);
home = g_file_new_for_path (g_get_home_dir ());
query = nautilus_query_new ();
nautilus_query_set_text (query, terms_joined);
nautilus_query_set_location (query, home);
return query;
}
static void
execute_search (NautilusShellSearchProvider *self,
GDBusMethodInvocation *invocation,
gchar **terms)
{
NautilusQuery *query;
PendingSearch *pending_search;
cancel_current_search (self);
/* don't attempt searches for a single character */
if (g_strv_length (terms) == 1 &&
g_utf8_strlen (terms[0], -1) == 1)
{
g_dbus_method_invocation_return_value (invocation, g_variant_new ("(as)", NULL));
return;
}
query = shell_query_new (terms);
nautilus_query_set_recursive (query, NAUTILUS_QUERY_RECURSIVE_INDEXED_ONLY);
nautilus_query_set_show_hidden_files (query, FALSE);
pending_search = g_slice_new0 (PendingSearch);
pending_search->invocation = g_object_ref (invocation);
pending_search->hits = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
pending_search->query = query;
pending_search->engine = nautilus_search_engine_new ();
pending_search->start_time = g_get_monotonic_time ();
pending_search->self = self;
g_signal_connect (pending_search->engine, "hits-added",
G_CALLBACK (search_hits_added_cb), pending_search);
g_signal_connect (pending_search->engine, "finished",
G_CALLBACK (search_finished_cb), pending_search);
g_signal_connect (pending_search->engine, "error",
G_CALLBACK (search_error_cb), pending_search);
self->current_search = pending_search;
g_application_hold (g_application_get_default ());
search_add_volumes_and_bookmarks (pending_search);
/* start searching */
g_debug ("*** Search engine search started");
nautilus_search_provider_set_query (NAUTILUS_SEARCH_PROVIDER (pending_search->engine),
query);
nautilus_search_provider_start (NAUTILUS_SEARCH_PROVIDER (pending_search->engine));
}
static gboolean
handle_get_initial_result_set (NautilusShellSearchProvider2 *skeleton,
GDBusMethodInvocation *invocation,
gchar **terms,
gpointer user_data)
{
NautilusShellSearchProvider *self = user_data;
g_debug ("****** GetInitialResultSet");
execute_search (self, invocation, terms);
return TRUE;
}
static gboolean
handle_get_subsearch_result_set (NautilusShellSearchProvider2 *skeleton,
GDBusMethodInvocation *invocation,
gchar **previous_results,
gchar **terms,
gpointer user_data)
{
NautilusShellSearchProvider *self = user_data;
g_debug ("****** GetSubSearchResultSet");
execute_search (self, invocation, terms);
return TRUE;
}
typedef struct
{
NautilusShellSearchProvider *self;
gint64 start_time;
GDBusMethodInvocation *invocation;
gchar **uris;
} ResultMetasData;
static void
result_metas_data_free (ResultMetasData *data)
{
g_clear_object (&data->self);
g_clear_object (&data->invocation);
g_strfreev (data->uris);
g_slice_free (ResultMetasData, data);
}
static void
result_metas_return_from_cache (ResultMetasData *data)
{
GVariantBuilder builder;
GVariant *meta;
gint64 current_time;
gint idx;
g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));
for (idx = 0; data->uris[idx] != NULL; idx++)
{
meta = g_hash_table_lookup (data->self->metas_cache,
data->uris[idx]);
g_variant_builder_add_value (&builder, meta);
}
current_time = g_get_monotonic_time ();
g_debug ("*** GetResultMetas completed - time elapsed %dms",
(gint) ((current_time - data->start_time) / 1000));
g_dbus_method_invocation_return_value (data->invocation,
g_variant_new ("(aa{sv})", &builder));
}
static void
result_list_attributes_ready_cb (GList *file_list,
gpointer user_data)
{
ResultMetasData *data = user_data;
GVariantBuilder meta;
NautilusFile *file;
GFile *file_location;
GList *l;
gchar *uri, *display_name;
gchar *path, *description;
gchar *thumbnail_path;
GIcon *gicon;
GFile *location;
GVariant *meta_variant;
gint icon_scale;
icon_scale = gdk_monitor_get_scale_factor (gdk_display_get_monitor (gdk_display_get_default (), 0));
for (l = file_list; l != NULL; l = l->next)
{
file = l->data;
g_variant_builder_init (&meta, G_VARIANT_TYPE ("a{sv}"));
uri = nautilus_file_get_uri (file);
display_name = get_display_name (data->self, file);
file_location = nautilus_file_get_location (file);
path = g_file_get_path (file_location);
description = path ? g_path_get_dirname (path) : NULL;
g_variant_builder_add (&meta, "{sv}",
"id", g_variant_new_string (uri));
g_variant_builder_add (&meta, "{sv}",
"name", g_variant_new_string (display_name));
/* Some backends like trash:/// don't have a path, so we show the uri itself. */
g_variant_builder_add (&meta, "{sv}",
"description", g_variant_new_string (description ? description : uri));
gicon = NULL;
thumbnail_path = nautilus_file_get_thumbnail_path (file);
if (thumbnail_path != NULL)
{
location = g_file_new_for_path (thumbnail_path);
gicon = g_file_icon_new (location);
g_free (thumbnail_path);
g_object_unref (location);
}
else
{
gicon = get_gicon (data->self, file);
}
if (gicon == NULL)
{
gicon = G_ICON (nautilus_file_get_icon_pixbuf (file, 128, TRUE,
icon_scale,
NAUTILUS_FILE_ICON_FLAGS_USE_THUMBNAILS));
}
g_variant_builder_add (&meta, "{sv}",
"icon", g_icon_serialize (gicon));
g_object_unref (gicon);
meta_variant = g_variant_builder_end (&meta);
g_hash_table_insert (data->self->metas_cache,
g_strdup (uri), g_variant_ref_sink (meta_variant));
g_free (display_name);
g_free (path);
g_free (description);
g_free (uri);
}
result_metas_return_from_cache (data);
result_metas_data_free (data);
}
static gboolean
handle_get_result_metas (NautilusShellSearchProvider2 *skeleton,
GDBusMethodInvocation *invocation,
gchar **results,
gpointer user_data)
{
NautilusShellSearchProvider *self = user_data;
GList *missing_files = NULL;
const gchar *uri;
ResultMetasData *data;
gint idx;
g_debug ("****** GetResultMetas");
for (idx = 0; results[idx] != NULL; idx++)
{
uri = results[idx];
if (!g_hash_table_lookup (self->metas_cache, uri))
{
missing_files = g_list_prepend (missing_files, nautilus_file_get_by_uri (uri));
}
}
data = g_slice_new0 (ResultMetasData);
data->self = g_object_ref (self);
data->invocation = g_object_ref (invocation);
data->start_time = g_get_monotonic_time ();
data->uris = g_strdupv (results);
if (missing_files == NULL)
{
result_metas_return_from_cache (data);
result_metas_data_free (data);
return TRUE;
}
nautilus_file_list_call_when_ready (missing_files,
NAUTILUS_FILE_ATTRIBUTES_FOR_ICON,
NULL,
result_list_attributes_ready_cb,
data);
nautilus_file_list_free (missing_files);
return TRUE;
}
static gboolean
handle_activate_result (NautilusShellSearchProvider2 *skeleton,
GDBusMethodInvocation *invocation,
gchar *result,
gchar **terms,
guint32 timestamp,
gpointer user_data)
{
gboolean res;
GFile *file;
res = gtk_show_uri_on_window (NULL, result, timestamp, NULL);
if (!res)
{
file = g_file_new_for_uri (result);
g_application_open (g_application_get_default (), &file, 1, "");
g_object_unref (file);
}
nautilus_shell_search_provider2_complete_activate_result (skeleton, invocation);
return TRUE;
}
static gboolean
handle_launch_search (NautilusShellSearchProvider2 *skeleton,
GDBusMethodInvocation *invocation,
gchar **terms,
guint32 timestamp,
gpointer user_data)
{
GApplication *app = g_application_get_default ();
g_autoptr (NautilusQuery) query = shell_query_new (terms);
NautilusQueryRecursive recursive = location_settings_search_get_recursive ();
/*
* If no recursive search is enabled, we still want to be able to
* show the same results we presented in the overview when nautilus
* is explicitly launched to access to more results, and thus we perform
* a query showing results coming from index-based search engines.
* Otherwise we respect the global setting for recursivity.
*/
if (recursive == NAUTILUS_QUERY_RECURSIVE_NEVER)
{
nautilus_query_set_recursive (query,
NAUTILUS_QUERY_RECURSIVE_INDEXED_ONLY);
}
else
{
nautilus_query_set_recursive (query, recursive);
}
nautilus_application_search (NAUTILUS_APPLICATION (app), query);
nautilus_shell_search_provider2_complete_launch_search (skeleton, invocation);
return TRUE;
}
static void
search_provider_dispose (GObject *obj)
{
NautilusShellSearchProvider *self = NAUTILUS_SHELL_SEARCH_PROVIDER (obj);
g_clear_object (&self->skeleton);
g_hash_table_destroy (self->metas_cache);
cancel_current_search (self);
G_OBJECT_CLASS (nautilus_shell_search_provider_parent_class)->dispose (obj);
}
static void
nautilus_shell_search_provider_init (NautilusShellSearchProvider *self)
{
self->metas_cache = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, (GDestroyNotify) g_variant_unref);
self->skeleton = nautilus_shell_search_provider2_skeleton_new ();
g_signal_connect (self->skeleton, "handle-get-initial-result-set",
G_CALLBACK (handle_get_initial_result_set), self);
g_signal_connect (self->skeleton, "handle-get-subsearch-result-set",
G_CALLBACK (handle_get_subsearch_result_set), self);
g_signal_connect (self->skeleton, "handle-get-result-metas",
G_CALLBACK (handle_get_result_metas), self);
g_signal_connect (self->skeleton, "handle-activate-result",
G_CALLBACK (handle_activate_result), self);
g_signal_connect (self->skeleton, "handle-launch-search",
G_CALLBACK (handle_launch_search), self);
}
static void
nautilus_shell_search_provider_class_init (NautilusShellSearchProviderClass *klass)
{
GObjectClass *oclass = G_OBJECT_CLASS (klass);
oclass->dispose = search_provider_dispose;
}
NautilusShellSearchProvider *
nautilus_shell_search_provider_new (void)
{
return g_object_new (nautilus_shell_search_provider_get_type (),
NULL);
}
gboolean
nautilus_shell_search_provider_register (NautilusShellSearchProvider *self,
GDBusConnection *connection,
GError **error)
{
return g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->skeleton),
connection,
"/org/gnome/Nautilus" PROFILE "/SearchProvider", error);
}
void
nautilus_shell_search_provider_unregister (NautilusShellSearchProvider *self)
{
g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->skeleton));
}