From 96fdd4f17237dc55be5e2a9cde5a20a23ce53086 Mon Sep 17 00:00:00 2001 From: Felipe Barriga Richards Date: Tue, 7 Mar 2017 00:53:00 -0300 Subject: search: added tag support (xattr::xdg.tags). --- libcaja-private/caja-query.c | 51 +++++++++ libcaja-private/caja-query.h | 4 + libcaja-private/caja-search-engine-simple.c | 159 ++++++++++++++++++++++++++-- src/caja-query-editor.c | 123 +++++++++++++++++++++ 4 files changed, 328 insertions(+), 9 deletions(-) diff --git a/libcaja-private/caja-query.c b/libcaja-private/caja-query.c index e0359a04..a5e783e4 100644 --- a/libcaja-private/caja-query.c +++ b/libcaja-private/caja-query.c @@ -35,6 +35,7 @@ struct CajaQueryDetails char *text; char *location_uri; GList *mime_types; + GList *tags; }; static void caja_query_class_init (CajaQueryClass *class); @@ -129,6 +130,29 @@ caja_query_add_mime_type (CajaQuery *query, const char *mime_type) g_strdup (mime_type)); } +GList * +caja_query_get_tags (CajaQuery *query) +{ + return eel_g_str_list_copy (query->details->tags); +} + +void +caja_query_set_tags (CajaQuery *query, GList *tags) +{ + g_list_free_full (query->details->tags, g_free); + query->details->tags = eel_g_str_list_copy (tags); +} + +void +caja_query_add_tag (CajaQuery *query, const char *tag) +{ + gchar *normalized = g_utf8_normalize (tag, -1, G_NORMALIZE_NFD); + gchar *lower_case = g_utf8_strdown (normalized, -1); + + g_free (normalized); + query->details->tags = g_list_append (query->details->tags, lower_case); +} + char * caja_query_to_readable_string (CajaQuery *query) { @@ -196,6 +220,8 @@ typedef struct gboolean in_location; gboolean in_mimetypes; gboolean in_mimetype; + gboolean in_tags; + gboolean in_tag; } ParserInfo; static void @@ -218,6 +244,10 @@ start_element_cb (GMarkupParseContext *ctx, info->in_mimetypes = TRUE; else if (strcmp (element_name, "mimetype") == 0) info->in_mimetype = TRUE; + else if (strcmp (element_name, "tags") == 0) + info->in_tags = TRUE; + else if (strcmp (element_name, "tag") == 0) + info->in_tag = TRUE; } static void @@ -238,6 +268,10 @@ end_element_cb (GMarkupParseContext *ctx, info->in_mimetypes = FALSE; else if (strcmp (element_name, "mimetype") == 0) info->in_mimetype = FALSE; + else if (strcmp (element_name, "tags") == 0) + info->in_tags = FALSE; + else if (strcmp (element_name, "tag") == 0) + info->in_tag = FALSE; } static void @@ -268,6 +302,10 @@ text_cb (GMarkupParseContext *ctx, { caja_query_add_mime_type (info->query, t); } + else if (info->in_tags && info->in_tag) + { + caja_query_add_tag (info->query, t); + } g_free (t); @@ -339,6 +377,7 @@ caja_query_to_xml (CajaQuery *query) char *text; char *uri; char *mimetype; + char *tag; GList *l; xml = g_string_new (""); @@ -369,6 +408,18 @@ caja_query_to_xml (CajaQuery *query) g_string_append (xml, " \n"); } + if (query->details->tags) + { + g_string_append (xml, " \n"); + for (l = query->details->tags; l != NULL; l = l->next) + { + tag = g_markup_escape_text (l->data, -1); + g_string_append_printf (xml, " %s\n", tag); + g_free (tag); + } + g_string_append (xml, " \n"); + } + g_string_append (xml, "\n"); return g_string_free (xml, FALSE); diff --git a/libcaja-private/caja-query.h b/libcaja-private/caja-query.h index a13c930e..8e0f1047 100644 --- a/libcaja-private/caja-query.h +++ b/libcaja-private/caja-query.h @@ -57,6 +57,10 @@ void caja_query_set_text (CajaQuery *query, const char *text char * caja_query_get_location (CajaQuery *query); void caja_query_set_location (CajaQuery *query, const char *uri); +GList * caja_query_get_tags (CajaQuery *query); +void caja_query_set_tags (CajaQuery *query, GList *tags); +void caja_query_add_tag (CajaQuery *query, const char *tag); + GList * caja_query_get_mime_types (CajaQuery *query); void caja_query_set_mime_types (CajaQuery *query, GList *mime_types); void caja_query_add_mime_type (CajaQuery *query, const char *mime_type); diff --git a/libcaja-private/caja-search-engine-simple.c b/libcaja-private/caja-search-engine-simple.c index e4078b36..016b03e6 100644 --- a/libcaja-private/caja-search-engine-simple.c +++ b/libcaja-private/caja-search-engine-simple.c @@ -38,6 +38,7 @@ typedef struct GCancellable *cancellable; GList *mime_types; + GList *tags; char **words; GList *found_list; @@ -121,6 +122,7 @@ search_thread_data_new (CajaSearchEngineSimple *engine, g_free (lower); g_free (normalized); + data->tags = caja_query_get_tags (query); data->mime_types = caja_query_get_mime_types (query); data->cancellable = g_cancellable_new (); @@ -137,6 +139,7 @@ search_thread_data_free (SearchThreadData *data) g_hash_table_destroy (data->visited); g_object_unref (data->cancellable); g_strfreev (data->words); + g_list_free_full (data->tags, g_free); g_list_free_full (data->mime_types, g_free); g_list_free_full (data->uri_hits, g_free); g_free (data); @@ -203,6 +206,8 @@ send_batch (SearchThreadData *data) data->uri_hits = NULL; } +#define G_FILE_ATTRIBUTE_XATTR_XDG_TAGS "xattr::xdg.tags" + #define STD_ATTRIBUTES \ G_FILE_ATTRIBUTE_STANDARD_NAME "," \ G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," \ @@ -210,13 +215,131 @@ send_batch (SearchThreadData *data) G_FILE_ATTRIBUTE_STANDARD_TYPE "," \ G_FILE_ATTRIBUTE_ID_FILE + +/* Stolen code + * file: glocalfileinfo.c + * function: hex_unescape_string + * GIO - GLib Input, Output and Streaming Library + */ +static char * +hex_unescape_string (const char *str, + int *out_len, + gboolean *free_return) +{ + int i; + char *unescaped_str, *p; + unsigned char c; + int len; + + len = strlen (str); + + if (strchr (str, '\\') == NULL) + { + if (out_len) + *out_len = len; + *free_return = FALSE; + return (char *)str; + } + + unescaped_str = g_malloc (len + 1); + + p = unescaped_str; + for (i = 0; i < len; i++) + { + if (str[i] == '\\' && + str[i+1] == 'x' && + len - i >= 4) + { + c = + (g_ascii_xdigit_value (str[i+2]) << 4) | + g_ascii_xdigit_value (str[i+3]); + *p++ = c; + i += 3; + } + else + *p++ = str[i]; + } + *p++ = 0; + + if (out_len) + *out_len = p - unescaped_str; + *free_return = TRUE; + return unescaped_str; +} +/* End of stolen code */ + +static inline gchar ** +get_tags_from_info (GFileInfo *info) +{ + char **result; + const gchar *escaped_tags_string + = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_XATTR_XDG_TAGS); + + gboolean new_created; + gchar *tags_string = hex_unescape_string (escaped_tags_string, + NULL, + &new_created); + + gchar *normalized = g_utf8_normalize (tags_string, -1, G_NORMALIZE_NFD); + + if (new_created) + g_free (tags_string); + + gchar *lower_case = g_utf8_strdown (normalized, -1); + g_free (normalized); + + result = g_strsplit (lower_case, ",", -1); + g_free (lower_case); + + return result; +} + +static inline gboolean +file_has_all_tags (GFileInfo *info, GList *tags) +{ + if (g_list_length (tags) == 0) { + return TRUE; + } + + if (!g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_XATTR_XDG_TAGS)) + { + return FALSE; + } + + char **file_tags = get_tags_from_info (info); + + guint file_tags_len = g_strv_length (file_tags); + if (file_tags_len < g_list_length (tags)) { + g_strfreev (file_tags); + return FALSE; + } + + for (GList *l = tags; l != NULL; l = l->next) { + gboolean found = FALSE; + for (int i = 0; i < file_tags_len; ++i) { + if (g_strcmp0 (l->data, file_tags[i]) == 0) { + found = TRUE; + break; + } + } + + if (found == FALSE) { + g_strfreev (file_tags); + return FALSE; + } + } + + g_strfreev (file_tags); + return TRUE; +} + static void visit_directory (GFile *dir, SearchThreadData *data) { GFileEnumerator *enumerator; GFileInfo *info; GFile *child; - const char *mime_type, *display_name; + const char *tag, *mime_type, *display_name; char *lower_name, *normalized; gboolean hit; int i; @@ -224,14 +347,27 @@ visit_directory (GFile *dir, SearchThreadData *data) const char *id; gboolean visited; - enumerator = g_file_enumerate_children (dir, - data->mime_types != NULL ? - STD_ATTRIBUTES "," - G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE - : - STD_ATTRIBUTES - , - 0, data->cancellable, NULL); + const char *attributes; + if (data->mime_types != NULL) { + if (data->tags != NULL) { + attributes = STD_ATTRIBUTES "," + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," + G_FILE_ATTRIBUTE_XATTR_XDG_TAGS; + } else { + attributes = STD_ATTRIBUTES "," + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE; + } + } else { + if (data->tags != NULL) { + attributes = STD_ATTRIBUTES "," + G_FILE_ATTRIBUTE_XATTR_XDG_TAGS; + } else { + attributes = STD_ATTRIBUTES; + } + } + + enumerator = g_file_enumerate_children (dir, attributes, 0, + data->cancellable, NULL); if (enumerator == NULL) { @@ -281,6 +417,11 @@ visit_directory (GFile *dir, SearchThreadData *data) } } + if (hit && data->tags) + { + hit = file_has_all_tags (info, data->tags); + } + child = g_file_get_child (dir, g_file_info_get_name (info)); if (hit) diff --git a/src/caja-query-editor.c b/src/caja-query-editor.c index 8319db25..5bfbf7ae 100644 --- a/src/caja-query-editor.c +++ b/src/caja-query-editor.c @@ -38,6 +38,7 @@ typedef enum { CAJA_QUERY_EDITOR_ROW_LOCATION, CAJA_QUERY_EDITOR_ROW_TYPE, + CAJA_QUERY_EDITOR_ROW_TAGS, CAJA_QUERY_EDITOR_ROW_LAST } CajaQueryEditorRowType; @@ -95,6 +96,8 @@ static guint signals[LAST_SIGNAL] = { 0 }; static void caja_query_editor_class_init (CajaQueryEditorClass *class); static void caja_query_editor_init (CajaQueryEditor *editor); +static void go_search_cb (GtkButton *clicked_button, CajaQueryEditor *editor); + static void entry_activate_cb (GtkWidget *entry, CajaQueryEditor *editor); static void entry_changed_cb (GtkWidget *entry, CajaQueryEditor *editor); static void caja_query_editor_changed_force (CajaQueryEditor *editor, @@ -109,6 +112,14 @@ static void location_row_add_to_query (CajaQueryEditorRow *row, static void location_row_free_data (CajaQueryEditorRow *row); static void location_add_rows_from_query (CajaQueryEditor *editor, CajaQuery *query); + +static GtkWidget *tags_row_create_widgets (CajaQueryEditorRow *row); +static void tags_row_add_to_query (CajaQueryEditorRow *row, + CajaQuery *query); +static void tags_row_free_data (CajaQueryEditorRow *row); +static void tags_add_rows_from_query (CajaQueryEditor *editor, + CajaQuery *query); + static GtkWidget *type_row_create_widgets (CajaQueryEditorRow *row); static void type_row_add_to_query (CajaQueryEditorRow *row, CajaQuery *query); @@ -134,6 +145,13 @@ static CajaQueryEditorRowOps row_type[] = type_row_free_data, type_add_rows_from_query }, + { + N_("Tags"), + tags_row_create_widgets, + tags_row_add_to_query, + tags_row_free_data, + tags_add_rows_from_query + } }; EEL_CLASS_BOILERPLATE (CajaQueryEditor, @@ -344,6 +362,111 @@ location_add_rows_from_query (CajaQueryEditor *editor, g_free (folder); } +/* Tags */ +static void +tags_entry_changed_cb (GtkWidget *entry, gpointer *data) +{ + /* remove commas from string */ + const gchar *text = gtk_entry_get_text ( GTK_ENTRY (entry)); + if (g_strrstr (text, ",") == NULL) { + return; + } + + gchar **words = g_strsplit (text, ",", -1); + gchar *sanitized = g_strjoinv ("", words); + g_strfreev (words); + + gtk_entry_set_text (GTK_ENTRY (entry), sanitized); + g_free(sanitized); +} + +#define MAX_TAGS_ENTRY_LEN 4096 // arbitrary value. + +static GtkWidget * +tags_row_create_widgets (CajaQueryEditorRow *row) +{ + GtkWidget *entry = gtk_entry_new(); + gtk_entry_set_max_length (GTK_ENTRY (entry), MAX_TAGS_ENTRY_LEN); + gtk_widget_set_tooltip_text (entry, + _("Tags separated by spaces. " + "Matches files that contains ALL specified tags.")); + + gtk_entry_set_placeholder_text (GTK_ENTRY (entry), + _("Tags separated by spaces. " + "Matches files that contains ALL specified tags.")); + + gtk_widget_show (entry); + gtk_box_pack_start (GTK_BOX (row->hbox), entry, TRUE, TRUE, 0); + g_signal_connect (entry, "changed", G_CALLBACK (tags_entry_changed_cb), entry); + g_signal_connect (entry, "activate", G_CALLBACK (go_search_cb), row->editor); + + return entry; +} + +static void +tags_row_add_to_query (CajaQueryEditorRow *row, + CajaQuery *query) +{ + GtkEntry *entry = GTK_ENTRY (row->type_widget); + const gchar *tags = gtk_entry_get_text (entry); + + char **strv = g_strsplit (tags, " ", -1); + guint len = g_strv_length (strv); + for (int i=0; i 0) { + caja_query_add_tag (query, strv[i]); + } + } + g_strfreev (strv); +} + +static void +tags_row_free_data (CajaQueryEditorRow *row) +{ +} + +gchar * +xattr_tags_list_to_str (const GList *tags) +{ + gchar *result = NULL; + + const GList *tags_iter = NULL; + for (tags_iter = tags; tags_iter; tags_iter = tags_iter->next) { + gchar *tmp; + + if (result != NULL) { + tmp = g_strconcat (result, ",", tags_iter->data, NULL); + g_free (result); + } else { + tmp = g_strdup (tags_iter->data); + } + + result = tmp; + } + + return result; +} + +static void +tags_add_rows_from_query (CajaQueryEditor *editor, + CajaQuery *query) +{ + GList *tags = caja_query_get_tags (query); + if (tags == NULL) { + return; + } + + CajaQueryEditorRow *row; + row = caja_query_editor_add_row (editor, CAJA_QUERY_EDITOR_ROW_TAGS); + + const gchar *tags_str = xattr_tags_list_to_str (tags); + g_list_free_full (tags, g_free); + + gtk_entry_set_text (GTK_ENTRY (row->type_widget), tags_str); + g_free (tags_str); +} + /* Type */ -- cgit v1.2.1