From b9a9169656cae021c57797e00cd33b87abfd1543 Mon Sep 17 00:00:00 2001 From: Jehan Date: Tue, 16 Sep 2014 03:45:56 +0200 Subject: [PATCH] app: update action search to use glib API. I now normalize with g_str_tokenize_and_fold() which uses standard Unicode normalization. I don't use g_str_match_string() directly though, because I want to run additional checks to order the results by relevance. For instance I still want actions whose labels starts with the search string to be at the top, and results with same order as search token before those with a different order. Then results with match in the tooltip. Finally I also returns results with partial match in the label, and the rest in the tooltip, though at the bottom of the list. Other than that, this returns the same results as g_str_match_string() with a similar algorithm. In particular now we only match the start of tokens (a substring in the middle of a token won't match anymore). I kept the small 2-character trick matching the first letters of the first 2 words of the label, but I got rid of the fuzzy search (that none really found ever relevant anyway). --- app/dialogs/action-search-dialog.c | 279 ++++++++++++++--------------- app/widgets/gimpaction-history.c | 2 +- app/widgets/gimpaction-history.h | 2 +- 3 files changed, 140 insertions(+), 143 deletions(-) diff --git a/app/dialogs/action-search-dialog.c b/app/dialogs/action-search-dialog.c index 7d7cb4bece..aa4ba75444 100644 --- a/app/dialogs/action-search-dialog.c +++ b/app/dialogs/action-search-dialog.c @@ -94,13 +94,10 @@ static void action_search_add_to_results_list (GtkAction *a static void action_search_run_selected (SearchDialog *private); static void action_search_history_and_actions (const gchar *keyword, SearchDialog *private); -static gboolean action_fuzzy_match (gchar *string, - gchar *key); -static gchar * action_search_normalize_string (const gchar *str); static gboolean action_search_match_keyword (GtkAction *action, const gchar* keyword, gint *section, - gboolean match_fuzzy); + Gimp *gimp); static void action_search_hide (SearchDialog *private); @@ -583,7 +580,7 @@ action_search_history_and_actions (const gchar *keyword, ! GIMP_GUI_CONFIG (private->gimp->config)->search_show_unavailable) continue; - if (action_search_match_keyword (action, keyword, §ion, TRUE)) + if (action_search_match_keyword (action, keyword, §ion, private->gimp)) { GList *list3; @@ -613,61 +610,16 @@ action_search_history_and_actions (const gchar *keyword, g_list_free_full (history_actions, (GDestroyNotify) g_object_unref); } -/** - * Fuzzy search matching. - * Returns: TRUE if all the letters of `key` are found in `string`, - * in the same order (even with intermediate letters). - */ -static gboolean -action_fuzzy_match (gchar *string, - gchar *key) -{ - gchar *remaining_string = string; - - if (strlen (key) == 0 ) - return TRUE; - - if ((remaining_string = strchr (string, key[0])) != NULL ) - return action_fuzzy_match (remaining_string + 1, - key + 1); - else - return FALSE; -} - -/* - * Returns: a newly allocated lowercased string, which replaced any - * spacing characters into a single space and stripped out any leading - * and trailing space. - */ -static gchar * -action_search_normalize_string (const gchar *str) -{ - GRegex *spaces_regex; - gchar *normalized_str; - gint i; - - spaces_regex = g_regex_new ("[ \n\t\r]+", 0, 0, NULL); - normalized_str = g_regex_replace_literal (spaces_regex, str, -1, 0, " ", 0, NULL); - - g_regex_unref (spaces_regex); - - normalized_str = g_strstrip (normalized_str); - - for (i = 0 ; i < strlen (normalized_str); i++) - normalized_str[i] = tolower (normalized_str[i]); - - return normalized_str; -} - static gboolean action_search_match_keyword (GtkAction *action, const gchar *keyword, gint *section, - gboolean match_fuzzy) + Gimp *gimp) { - gboolean matched = FALSE; - gchar *key; - gchar *label; + gboolean matched = FALSE; + gchar **key_tokens; + gchar **label_tokens; + gchar **label_alternates = NULL; gchar *tmp; if (keyword == NULL) @@ -682,108 +634,153 @@ action_search_match_keyword (GtkAction *action, return TRUE; } - key = action_search_normalize_string (keyword); - tmp = gimp_strip_uline (gtk_action_get_label (action)); - label = action_search_normalize_string (tmp); + key_tokens = g_str_tokenize_and_fold (keyword, gimp->config->language, NULL); + tmp = gimp_strip_uline (gtk_action_get_label (action)); + label_tokens = g_str_tokenize_and_fold (tmp, gimp->config->language, &label_alternates); g_free (tmp); /* If keyword is two characters, then match them with first letters * of first and second word in the labels. For instance 'gb' will * list 'Gaussian Blur...' */ - if (strlen (key) == 2) + if (g_strv_length (key_tokens) == 1 && g_utf8_strlen (key_tokens[0], -1) == 2) { - gchar* space_pos; + gunichar c1 = g_utf8_get_char (key_tokens[0]); + gunichar c2 = g_utf8_get_char (g_utf8_find_next_char (key_tokens[0], NULL)); - space_pos = strchr (label, ' '); - - if (space_pos != NULL) - { - space_pos++; - - if (key[0] == label[0] && key[1] == *space_pos) - { - matched = TRUE; - if (section) - { - *section = 1; - } - } - } - } - - if (! matched) - { - gchar *substr; - - substr = strstr (label, key); - if (substr) + if ((g_strv_length (label_tokens) > 1 && + g_utf8_get_char (label_tokens[0]) == c1 && + g_utf8_get_char (label_tokens[1]) == c2) || + (g_strv_length (label_alternates) > 1 && + g_utf8_get_char (label_alternates[0]) == c1 && + g_utf8_get_char (label_alternates[1]) == c2)) { matched = TRUE; if (section) { - /* If the substring is the label start, this is a nicer match. */ - *section = (substr == label) ? 1 : 2; - } - } - else if (strlen (key) > 2) - { - gchar *tooltip = NULL; - - if (gtk_action_get_tooltip (action)!= NULL) - { - tooltip = action_search_normalize_string (gtk_action_get_tooltip (action)); - - if (strstr (tooltip, key)) - { - matched = TRUE; - if (section) - { - *section = 3; - } - } - } - - if (! matched && strchr (key, ' ')) - { - gchar **words; - gchar **word; - - matched = TRUE; - if (section) - { - *section = 4; - } - - words = g_strsplit (key, " ", 0); - for (word = &words[0]; *word; ++word) - { - if (! strstr (label, *word) && - (! tooltip || ! strstr (tooltip, *word))) - { - matched = FALSE; - break; - } - } - - g_strfreev (words); - } - - g_free (tooltip); - } - - if (! matched && match_fuzzy && action_fuzzy_match (label, key)) - { - matched = TRUE; - if (section) - { - *section = 5; + *section = 1; } } } - g_free (label); - g_free (key); + if (! matched && g_strv_length (label_tokens) > 0) + { + gint previous_matched = -1; + gboolean match_start; + gboolean match_ordered; + gint i; + + matched = TRUE; + match_start = TRUE; + match_ordered = TRUE; + for (i = 0; key_tokens[i] != NULL; i++) + { + gint j; + for (j = 0; label_tokens[j] != NULL; j++) + { + if (g_str_has_prefix (label_tokens[j], key_tokens[i])) + { + goto one_matched; + } + } + for (j = 0; label_alternates[j] != NULL; j++) + { + if (g_str_has_prefix (label_alternates[j], key_tokens[i])) + { + goto one_matched; + } + } + matched = FALSE; +one_matched: + if (previous_matched > j) + match_ordered = FALSE; + previous_matched = j; + + if (i != j) + match_start = FALSE; + + continue; + } + + if (matched && section) + { + /* If the key is the label start, this is a nicer match. + * Then if key tokens are found in the same order in the label. + * Finally we show at the end if the key tokens are found with a different order. */ + *section = match_ordered ? (match_start ? 1 : 2) : 3; + } + } + + if (! matched && g_utf8_strlen (key_tokens[0], -1) > 2 && + gtk_action_get_tooltip (action) != NULL) + { + gchar **tooltip_tokens; + gchar **tooltip_alternates = NULL; + gboolean mixed_match; + gint i; + + tooltip_tokens = g_str_tokenize_and_fold (gtk_action_get_tooltip (action), + gimp->config->language, &tooltip_alternates); + + if (g_strv_length (tooltip_tokens) > 0) + { + matched = TRUE; + mixed_match = FALSE; + + for (i = 0; key_tokens[i] != NULL; i++) + { + gint j; + for (j = 0; tooltip_tokens[j] != NULL; j++) + { + if (g_str_has_prefix (tooltip_tokens[j], key_tokens[i])) + { + goto one_tooltip_matched; + } + } + for (j = 0; tooltip_alternates[j] != NULL; j++) + { + if (g_str_has_prefix (tooltip_alternates[j], key_tokens[i])) + { + goto one_tooltip_matched; + } + } + for (j = 0; label_tokens[j] != NULL; j++) + { + if (g_str_has_prefix (label_tokens[j], key_tokens[i])) + { + mixed_match = TRUE; + goto one_tooltip_matched; + } + } + for (j = 0; label_alternates[j] != NULL; j++) + { + if (g_str_has_prefix (label_alternates[j], key_tokens[i])) + { + mixed_match = TRUE; + goto one_tooltip_matched; + } + } + matched = FALSE; +one_tooltip_matched: + continue; + } + if (matched && section) + { + /* Matching the tooltip is section 4. We don't go looking + * for start of string or token order for tooltip match. + * But if the match is mixed on tooltip and label (there are + * no match for *only* label or *only* tooltip), this is + * section 5. */ + *section = mixed_match ? 5 : 4; + } + } + g_strfreev (tooltip_tokens); + g_strfreev (tooltip_alternates); + } + + g_strfreev (key_tokens); + g_strfreev (label_tokens); + g_strfreev (label_alternates); return matched; } diff --git a/app/widgets/gimpaction-history.c b/app/widgets/gimpaction-history.c index b3176286f8..3a503d21ca 100644 --- a/app/widgets/gimpaction-history.c +++ b/app/widgets/gimpaction-history.c @@ -279,7 +279,7 @@ gimp_action_history_search (Gimp *gimp, ! config->search_show_unavailable) continue; - if (match_func (action, keyword, NULL, FALSE)) + if (match_func (action, keyword, NULL, gimp)) result = g_list_prepend (result, g_object_ref (action)); } diff --git a/app/widgets/gimpaction-history.h b/app/widgets/gimpaction-history.h index 1f0f402f12..2ac4464830 100644 --- a/app/widgets/gimpaction-history.h +++ b/app/widgets/gimpaction-history.h @@ -25,7 +25,7 @@ typedef gboolean (* GimpActionMatchFunc) (GtkAction *action, const gchar *keyword, gint *section, - gboolean match_fuzzy); + Gimp *gimp); void gimp_action_history_init (Gimp *gimp);