[gnome-characters] Rework search to allow multiple invocation



commit fbe6b6f973bbf56bd2599317434299b7c301e7a2
Author: Daiki Ueno <dueno src gnome org>
Date:   Wed Nov 11 12:25:28 2015 +0900

    Rework search to allow multiple invocation
    
    Now all search functions take a context argument, which remembers the
    last search and allows multiple invocations of a search function when
    more results are requested.
    
    From the character list UI, more results are requested when the user
    scrolls down the list to the bottom.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=757523

 lib/gc.c             |  715 +++++++++++++++++++++++++++++++-------------------
 lib/gc.h             |   98 +++++---
 src/character.js     |    9 +-
 src/characterList.js |  115 +++++----
 4 files changed, 573 insertions(+), 364 deletions(-)
---
diff --git a/lib/gc.c b/lib/gc.c
index 6f45cce..5c19748 100644
--- a/lib/gc.c
+++ b/lib/gc.c
@@ -18,9 +18,31 @@
 #define PANGO_ENABLE_ENGINE 1
 #include <pango/pangofc-font.h>
 
+static gsize all_blocks_initialized;
 static const uc_block_t *all_blocks;
 static size_t all_block_count;
 
+#define LATIN_BLOCK_SIZE 4
+static gsize latin_blocks_initialized;
+static const uc_block_t latin_blocks[LATIN_BLOCK_SIZE];
+static const ucs4_t latin_block_starters[LATIN_BLOCK_SIZE] =
+  { 0x0000, 0x0080, 0x0100, 0x0180 };
+static size_t latin_block_count;
+
+#define HIRAGANA_BLOCK_SIZE 1
+static gsize hiragana_blocks_initialized;
+static const uc_block_t hiragana_blocks[HIRAGANA_BLOCK_SIZE];
+static const ucs4_t hiragana_block_starters[HIRAGANA_BLOCK_SIZE] =
+  { 0x3040 };
+static size_t hiragana_block_count;
+
+#define KATAKANA_BLOCK_SIZE 2
+static gsize katakana_blocks_initialized;
+static const uc_block_t katakana_blocks[KATAKANA_BLOCK_SIZE];
+static const ucs4_t katakana_block_starters[KATAKANA_BLOCK_SIZE] =
+  { 0x30A0, 0x31F0 };
+static size_t katakana_block_count;
+
 /* Bullets are not specially categorized in the Unicode standard.
    Use the character list from UTR#25 "Unicode Support for Mathematics".  */
 static const gunichar bullet_characters[] =
@@ -90,11 +112,11 @@ struct GcCharacterIter
 static gboolean gc_character_iter_next (GcCharacterIter      *iter);
 static gunichar gc_character_iter_get  (GcCharacterIter      *iter);
 
-static void     gc_enumerate_character_by_category
+static void     gc_character_iter_init_for_category
                                        (GcCharacterIter      *iter,
                                         GcCategory            category);
 
-static void     gc_enumerate_character_by_keywords
+static void     gc_character_iter_init_for_keywords
                                        (GcCharacterIter      *iter,
                                         const gchar * const * keywords);
 
@@ -103,8 +125,8 @@ gc_character_iter_next (GcCharacterIter *iter)
 {
   ucs4_t uc = iter->uc;
 
-  /* First search in the explicit character list.  */
-  if (iter->character_index + 1 < iter->character_count)
+  /* First, search in the explicit character list.  */
+  if (iter->character_index < iter->character_count)
     {
       iter->uc = iter->characters[iter->character_index++];
       return TRUE;
@@ -232,11 +254,14 @@ gc_character_iter_init_for_scripts (GcCharacterIter   *iter,
 }
 
 static void
-gc_enumerate_character_by_category (GcCharacterIter *iter,
-                                    GcCategory       category)
+gc_character_iter_init_for_category (GcCharacterIter *iter,
+                                     GcCategory       category)
 {
-  if (!all_blocks)
-    uc_all_blocks (&all_blocks, &all_block_count);
+  if (g_once_init_enter (&all_blocks_initialized))
+    {
+      uc_all_blocks (&all_blocks, &all_block_count);
+      g_once_init_leave (&all_blocks_initialized, 1);
+    }
 
   switch (category)
     {
@@ -419,9 +444,15 @@ filter_keywords (GcCharacterIter *iter, ucs4_t uc)
 }
 
 static void
-gc_enumerate_character_by_keywords (GcCharacterIter      *iter,
-                                    const gchar * const * keywords)
+gc_character_iter_init_for_keywords (GcCharacterIter      *iter,
+                                     const gchar * const * keywords)
 {
+  if (g_once_init_enter (&all_blocks_initialized))
+    {
+      uc_all_blocks (&all_blocks, &all_block_count);
+      g_once_init_leave (&all_blocks_initialized, 1);
+    }
+
   gc_character_iter_init (iter);
   iter->blocks = all_blocks;
   iter->block_count = all_block_count;
@@ -446,12 +477,12 @@ gc_character_name (gunichar uc)
   if (g_once_init_enter (&cjk_blocks_initialized))
     {
       static const ucs4_t cjk_block_starters[6] =
-       {
-         0x4E00, 0x3400, 0x20000, 0x2A700, 0x2B740, 0x2B820
-       };
+        {
+          0x4E00, 0x3400, 0x20000, 0x2A700, 0x2B740, 0x2B820
+        };
 
       for (i = 0; i < G_N_ELEMENTS (cjk_block_starters); i++)
-       cjk_blocks[i] = uc_block (cjk_block_starters[i]);
+        cjk_blocks[i] = uc_block (cjk_block_starters[i]);
       g_once_init_leave (&cjk_blocks_initialized, 1);
     }
 
@@ -463,6 +494,12 @@ gc_character_name (gunichar uc)
   return unicode_character_name (uc, g_new0 (gchar, UNINAME_MAX));
 }
 
+GQuark
+gc_search_error_quark (void)
+{
+  return g_quark_from_static_string ("gc-search-error-quark");
+}
+
 G_DEFINE_BOXED_TYPE (GcSearchResult, gc_search_result,
                      g_array_ref, g_array_unref);
 
@@ -475,190 +512,162 @@ gc_search_result_get (GcSearchResult *result, gint index)
   return g_array_index (result, gunichar, index);
 }
 
-struct SearchData
+typedef enum
+  {
+    GC_SEARCH_CRITERIA_CATEGORY,
+    GC_SEARCH_CRITERIA_KEYWORDS,
+    GC_SEARCH_CRITERIA_SCRIPTS,
+    GC_SEARCH_CRITERIA_RELATED
+  } GcSearchCriteriaType;
+
+struct _GcSearchCriteria
 {
-  GcCategory category;
-  gchar **keywords;
-  const uc_script_t **scripts;
-  gunichar uc;
-  gint max_matches;
+  GcSearchCriteriaType type;
+
+  union {
+    GcCategory category;
+    gchar **keywords;
+    const uc_script_t **scripts;
+    gunichar related;
+  } u;
 };
 
-static void
-search_data_free (struct SearchData *data)
+static gpointer
+gc_search_criteria_copy (gpointer boxed)
 {
-  if (data->keywords)
-    g_strfreev (data->keywords);
-  if (data->scripts)
-    g_free (data->scripts);
-  g_slice_free (struct SearchData, data);
+  GcSearchCriteria *criteria = g_memdup (boxed, sizeof (GcSearchCriteria));
+
+  if (criteria->type == GC_SEARCH_CRITERIA_KEYWORDS)
+    criteria->u.keywords = g_strdupv (criteria->u.keywords);
+
+  return criteria;
 }
 
 static void
-gc_search_by_category_thread (GTask        *task,
-                              gpointer      source_object,
-                              gpointer      task_data,
-                              GCancellable *cancellable)
+gc_search_criteria_free (gpointer boxed)
 {
-  GcCharacterIter iter;
-  GArray *result;
-  struct SearchData *data = task_data;
-
-  if (!all_blocks)
-    uc_all_blocks (&all_blocks, &all_block_count);
+  GcSearchCriteria *criteria = boxed;
 
-  result = g_array_new (FALSE, FALSE, sizeof (gunichar));
-  gc_enumerate_character_by_category (&iter, data->category);
-  while (!g_cancellable_is_cancelled (cancellable)
-         && gc_character_iter_next (&iter))
-    {
-      gunichar uc = gc_character_iter_get (&iter);
-      if (data->max_matches < 0 || result->len < data->max_matches)
-        g_array_append_val (result, uc);
-    }
+  if (criteria->type == GC_SEARCH_CRITERIA_KEYWORDS)
+    g_strfreev (criteria->u.keywords);
 
-  g_task_return_pointer (task, result, (GDestroyNotify) g_array_unref);
+  g_free (criteria);
 }
 
+G_DEFINE_BOXED_TYPE (GcSearchCriteria, gc_search_criteria,
+                     gc_search_criteria_copy, gc_search_criteria_free)
+
 /**
- * gc_search_by_category:
- * @category: a #GcCategory.
- * @max_matches: the maximum number of results.
- * @cancellable: a #GCancellable.
- * @callback: a #GAsyncReadyCallback.
- * @user_data: a user data passed to @callback.
+ * gc_search_criteria_new_category:
+ * @category: a #GcCategory
+ *
+ * Returns: (transfer full): a new #GcSearchCriteria
  */
-void
-gc_search_by_category (GcCategory          category,
-                       gint                max_matches,
-                       GCancellable       *cancellable,
-                       GAsyncReadyCallback callback,
-                       gpointer            user_data)
+GcSearchCriteria *
+gc_search_criteria_new_category (GcCategory category)
 {
-  GTask *task;
-  struct SearchData *data;
-
-  task = g_task_new (NULL, cancellable, callback, user_data);
+  GcSearchCriteria *result = g_new0 (GcSearchCriteria, 1);
+  result->type = GC_SEARCH_CRITERIA_CATEGORY;
+  result->u.category = category;
+  return result;
+}
 
-  data = g_slice_new0 (struct SearchData);
-  data->category = category;
-  data->max_matches = max_matches;
-  g_task_set_task_data (task, data,
-                        (GDestroyNotify) search_data_free);
-  g_task_run_in_thread (task, gc_search_by_category_thread);
+/**
+ * gc_search_criteria_new_keywords:
+ * @keywords: (array zero-terminated=1) (element-type utf8): an array of keywords
+ *
+ * Returns: (transfer full): a new #GcSearchCriteria
+ */
+GcSearchCriteria *
+gc_search_criteria_new_keywords (const gchar * const * keywords)
+{
+  GcSearchCriteria *result = g_new0 (GcSearchCriteria, 1);
+  result->type = GC_SEARCH_CRITERIA_KEYWORDS;
+  result->u.keywords = g_strdupv ((gchar **) keywords);
+  return result;
 }
 
-static void
-gc_search_by_keywords_thread (GTask        *task,
-                              gpointer      source_object,
-                              gpointer      task_data,
-                              GCancellable *cancellable)
+GcSearchCriteria *
+gc_search_criteria_new_scripts (const gchar * const * scripts)
 {
-  GcCharacterIter iter;
-  GArray *result;
-  struct SearchData *data = task_data;
-  const gchar * const * keywords = (const gchar * const *) data->keywords;
+  GcSearchCriteria *result = g_new0 (GcSearchCriteria, 1);
+  guint length, i;
 
-  if (!all_blocks)
-    uc_all_blocks (&all_blocks, &all_block_count);
+  result->type = GC_SEARCH_CRITERIA_SCRIPTS;
 
-  result = g_array_new (FALSE, FALSE, sizeof (gunichar));
-  gc_enumerate_character_by_keywords (&iter, keywords);
-  while (!g_cancellable_is_cancelled (cancellable)
-         && gc_character_iter_next (&iter))
-    {
-      gunichar uc = gc_character_iter_get (&iter);
-      if (data->max_matches < 0 || result->len < data->max_matches)
-        g_array_append_val (result, uc);
-    }
+  length = g_strv_length ((gchar **) scripts);
+  result->u.scripts = g_malloc0_n (length + 1, sizeof (uc_script_t *));
+  for (i = 0; i < length; i++)
+    result->u.scripts[i] = uc_script_byname (scripts[i]);
 
-  g_task_return_pointer (task, result, (GDestroyNotify) g_array_unref);
+  return result;
 }
 
-/**
- * gc_search_by_keywords:
- * @keywords: (array zero-terminated=1) (element-type utf8): an array of keywords
- * @max_matches: the maximum number of results.
- * @cancellable: a #GCancellable.
- * @callback: a #GAsyncReadyCallback.
- * @user_data: a user data passed to @callback.
- */
-void
-gc_search_by_keywords (const gchar * const * keywords,
-                       gint                  max_matches,
-                       GCancellable         *cancellable,
-                       GAsyncReadyCallback   callback,
-                       gpointer              user_data)
+GcSearchCriteria *
+gc_search_criteria_new_related (gunichar uc)
 {
-  GTask *task;
-  struct SearchData *data;
-
-  task = g_task_new (NULL, cancellable, callback, user_data);
-
-  data = g_slice_new0 (struct SearchData);
-  data->keywords = g_strdupv ((gchar **) keywords);
-  data->max_matches = max_matches;
-  g_task_set_task_data (task, data,
-                        (GDestroyNotify) search_data_free);
-  g_task_run_in_thread (task, gc_search_by_keywords_thread);
+  GcSearchCriteria *result = g_new0 (GcSearchCriteria, 1);
+  result->type = GC_SEARCH_CRITERIA_RELATED;
+  result->u.related = uc;
+  return result;
 }
 
-static void
-gc_search_by_scripts_thread (GTask        *task,
-                             gpointer      source_object,
-                             gpointer      task_data,
-                             GCancellable *cancellable)
+enum GcSearchState
+  {
+    GC_SEARCH_STATE_NOT_STARTED,
+    GC_SEARCH_STATE_STEP_STARTED,
+    GC_SEARCH_STATE_STEP_FINISHED,
+    GC_SEARCH_STATE_FINISHED
+  };
+
+struct _GcSearchContext
 {
+  GObject parent;
+  GMutex lock;
+  enum GcSearchState state;
   GcCharacterIter iter;
-  GArray *result;
-  struct SearchData *data = task_data;
-
-  if (!all_blocks)
-    uc_all_blocks (&all_blocks, &all_block_count);
+  GcSearchCriteria *criteria;
+};
 
-  result = g_array_new (FALSE, FALSE, sizeof (gunichar));
-  gc_character_iter_init_for_scripts (&iter,
-                                      (const uc_script_t * const *) data->scripts);
-  while (!g_cancellable_is_cancelled (cancellable)
-         && gc_character_iter_next (&iter))
-    {
-      gunichar uc = gc_character_iter_get (&iter);
-      g_array_append_val (result, uc);
-    }
+struct SearchData
+{
+  GcCategory category;
+  gchar **keywords;
+  const uc_script_t **scripts;
+  gunichar uc;
+  gint max_matches;
+  GcSearchContext *context;
+};
 
-  g_task_return_pointer (task, result, (GDestroyNotify) g_array_unref);
+static void
+search_data_free (struct SearchData *data)
+{
+  g_clear_pointer (&data->keywords, (GDestroyNotify) g_strfreev);
+  g_clear_pointer (&data->scripts, g_free);
+  g_clear_object (&data->context);
+  g_slice_free (struct SearchData, data);
 }
 
-/**
- * gc_search_by_scripts:
- * @scripts: (array zero-terminated=1) (element-type utf8): an array of scripts
- * @max_matches: the maximum number of results.
- * @cancellable: a #GCancellable.
- * @callback: a #GAsyncReadyCallback.
- * @user_data: a user data passed to @callback.
- */
-void
-gc_search_by_scripts (const gchar * const * scripts,
-                      gint                  max_matches,
-                      GCancellable         *cancellable,
-                      GAsyncReadyCallback   callback,
-                      gpointer              user_data)
+static void
+add_composited (GArray *result, ucs4_t base,
+                const uc_block_t *blocks, size_t count)
 {
-  GTask *task;
-  struct SearchData *data;
-  guint length, i;
+  size_t i;
 
-  task = g_task_new (NULL, cancellable, callback, user_data);
+  for (i = 0; i < count; i++)
+    {
+      const uc_block_t *block = &blocks[i];
+      ucs4_t uc;
 
-  data = g_slice_new0 (struct SearchData);
-  length = g_strv_length ((gchar **) scripts);
-  data->scripts = g_malloc0_n (length + 1, sizeof (uc_script_t *));
-  for (i = 0; i < length; i++)
-    data->scripts[i] = uc_script_byname (scripts[i]);
-  data->max_matches = max_matches;
-  g_task_set_task_data (task, data,
-                        (GDestroyNotify) search_data_free);
-  g_task_run_in_thread (task, gc_search_by_scripts_thread);
+      for (uc = 0; uc < block->end; uc++)
+        {
+          ucs4_t decomposition[UC_DECOMPOSITION_MAX_LENGTH];
+
+          uc_canonical_decomposition (uc, decomposition);
+          if (decomposition[0] == base)
+            g_array_append_val (result, uc);
+        }
+    }
 }
 
 static int
@@ -670,13 +679,11 @@ confusable_character_class_compare (const void *a,
 }
 
 static void
-gc_add_confusables (struct SearchData *data,
-                    GArray            *result,
-                    GCancellable      *cancellable)
+add_confusables (GArray *result, ucs4_t uc)
 {
   struct ConfusableCharacterClass key, *res;
 
-  key.uc = data->uc;
+  key.uc = uc;
   res = bsearch (&key, confusable_character_classes,
                  G_N_ELEMENTS (confusable_character_classes),
                  sizeof (*confusable_character_classes),
@@ -684,15 +691,9 @@ gc_add_confusables (struct SearchData *data,
   if (res)
     {
       const struct ConfusableClass *klass = &confusable_classes[res->index];
-      uint16_t i;
-
-      for (i = 0; i < klass->length
-             && !g_cancellable_is_cancelled (cancellable); i++)
-        {
-          gunichar uc = confusable_characters[klass->offset + i];
-          if (data->max_matches < 0 || result->len < data->max_matches)
-            g_array_append_val (result, uc);
-        }
+      g_array_append_vals (result,
+                           &confusable_characters[klass->offset],
+                           klass->length);
     }
 }
 
@@ -729,97 +730,64 @@ remove_duplicates (GArray *array)
 }
 
 static void
-add_composited (GArray *result,
-                ucs4_t uc,
-                ucs4_t *block_characters,
-                size_t n_block_characters)
-{
-  ucs4_t decomposition[UC_DECOMPOSITION_MAX_LENGTH];
-  int decomposition_length;
-  ucs4_t base;
-  size_t i;
-
-  decomposition_length = uc_canonical_decomposition (uc, decomposition);
-  if (decomposition_length > 0)
-    {
-      base = decomposition[0];
-      g_array_append_val (result, base);
-    }
-  else
-    base = uc;
-
-  for (i = 0; i < n_block_characters; i++)
-    {
-      const uc_block_t *block;
-
-      block = uc_block (block_characters[i]);
-      for (uc = block->start; uc < block->end; uc++)
-        {
-          decomposition_length = uc_canonical_decomposition (uc, decomposition);
-          if (decomposition_length > 0 && decomposition[0] == base)
-            g_array_append_val (result, uc);
-        }
-    }
-}
-
-static void
-gc_search_related_thread (GTask        *task,
-                          gpointer      source_object,
-                          gpointer      task_data,
-                          GCancellable *cancellable)
+populate_related_characters (GcCharacterIter *iter)
 {
   GArray *result;
-  struct SearchData *data = task_data;
-  ucs4_t mirror;
-  gunichar uc;
-  gint i;
+  ucs4_t related;
+  size_t i;
 
   result = g_array_new (FALSE, FALSE, sizeof (gunichar));
 
-  uc = uc_toupper (data->uc);
-  g_array_append_val (result, uc);
+  related = uc_toupper (iter->uc);
+  if (related != iter->uc)
+    g_array_append_val (result, related);
 
-  uc = uc_tolower (data->uc);
-  g_array_append_val (result, uc);
+  related = uc_tolower (iter->uc);
+  if (related != iter->uc)
+    g_array_append_val (result, related);
 
-  uc = uc_totitle (data->uc);
-  g_array_append_val (result, uc);
+  related = uc_totitle (iter->uc);
+  if (related != iter->uc)
+    g_array_append_val (result, related);
 
-  if (uc_mirror_char (data->uc, &mirror))
-    {
-      uc = mirror;
-      g_array_append_val (result, uc);
-    }
+  if (uc_mirror_char (iter->uc, &related) && related != iter->uc)
+    g_array_append_val (result, related);
 
-  if (uc_is_general_category (data->uc, UC_CATEGORY_L))
+  if (uc_is_general_category (iter->uc, UC_CATEGORY_L))
     {
+      ucs4_t decomposition[UC_DECOMPOSITION_MAX_LENGTH];
+      int decomposition_length;
+      ucs4_t decomposition_base;
       const uc_script_t *script;
 
-      script = uc_script (data->uc);
+      decomposition_length =
+        uc_canonical_decomposition (iter->uc, decomposition);
+      if (decomposition_length > 0)
+        {
+          decomposition_base = decomposition[0];
+          if (decomposition_base != iter->uc)
+            g_array_append_val (result, decomposition_base);
+        }
+      else
+        decomposition_base = iter->uc;
+
+      script = uc_script (iter->uc);
       if (script)
         {
           if (strcmp (script->name, "Latin") == 0)
-            {
-              ucs4_t block_starters[4] = { 0x0000, 0x0080, 0x0100, 0x0180 };
-              add_composited (result, data->uc,
-                              block_starters, G_N_ELEMENTS (block_starters));
-            }
+            add_composited (result, decomposition_base,
+                            latin_blocks, latin_block_count);
           else if (strcmp (script->name, "Hiragana") == 0)
-            {
-              ucs4_t block_starters[1] = { 0x3040 };
-              add_composited (result, data->uc,
-                              block_starters, G_N_ELEMENTS (block_starters));
-            }
+            add_composited (result, decomposition_base,
+                            hiragana_blocks, hiragana_block_count);
           else if (strcmp (script->name, "Katakana") == 0)
-            {
-              ucs4_t block_starters[2] = { 0x30A0, 0x31F0 };
-              add_composited (result, data->uc,
-                              block_starters, G_N_ELEMENTS (block_starters));
-            }
+            add_composited (result, decomposition_base,
+                            katakana_blocks, katakana_block_count);
         }
     }
 
-  gc_add_confusables (data, result, cancellable);
+  add_confusables (result, iter->uc);
+
   g_array_sort (result, compare_unichar);
   remove_duplicates (result);
 
@@ -828,60 +796,269 @@ gc_search_related_thread (GTask        *task,
       gunichar *puc;
 
       puc = &g_array_index (result, gunichar, i);
-      if (*puc == data->uc)
+      if (*puc == iter->uc)
         {
           g_array_remove_index (result, i);
           break;
         }
     }
 
+  iter->character_count = result->len;
+  iter->characters = (gunichar *) g_array_free (result, FALSE);
+}
+
+static size_t
+init_blocks (const uc_block_t *blocks, const ucs4_t *starters, size_t size)
+{
+  size_t i, count;
+
+  for (i = 0, count = 0; i < size; i++)
+    {
+      const uc_block_t *block = uc_block (starters[i]);
+      if (block)
+        memcpy ((uc_block_t *) &blocks[count++], block, sizeof (uc_block_t));
+    }
+  return count;
+}
+
+static void
+gc_character_iter_init_for_related (GcCharacterIter *iter,
+                                    gunichar         uc)
+{
+  if (g_once_init_enter (&latin_blocks_initialized))
+    {
+      latin_block_count =
+        init_blocks (latin_blocks, latin_block_starters,
+                     LATIN_BLOCK_SIZE);
+      g_once_init_leave (&latin_blocks_initialized, 1);
+    }
+
+  if (g_once_init_enter (&hiragana_blocks_initialized))
+    {
+      hiragana_block_count =
+        init_blocks (hiragana_blocks, hiragana_block_starters,
+                     HIRAGANA_BLOCK_SIZE);
+      g_once_init_leave (&hiragana_blocks_initialized, 1);
+    }
+
+  if (g_once_init_enter (&katakana_blocks_initialized))
+    {
+      katakana_block_count =
+        init_blocks (katakana_blocks, katakana_block_starters,
+                     KATAKANA_BLOCK_SIZE);
+      g_once_init_leave (&katakana_blocks_initialized, 1);
+    }
+
+  gc_character_iter_init (iter);
+  iter->uc = uc;
+  populate_related_characters (iter);
+}
+
+enum {
+  SEARCH_CONTEXT_PROP_0,
+  SEARCH_CONTEXT_PROP_CRITERIA,
+  SEARCH_CONTEXT_LAST_PROP
+};
+
+static GParamSpec *search_context_props[SEARCH_CONTEXT_LAST_PROP] = { NULL, };
+
+G_DEFINE_TYPE (GcSearchContext, gc_search_context, G_TYPE_OBJECT);
+
+static void
+gc_search_context_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  GcSearchContext *context = GC_SEARCH_CONTEXT (object);
+
+  switch (prop_id)
+    {
+    case SEARCH_CONTEXT_PROP_CRITERIA:
+      context->criteria = g_value_dup_boxed (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gc_search_context_finalize (GObject *object)
+{
+  GcSearchContext *context = GC_SEARCH_CONTEXT (object);
+
+  g_mutex_clear (&context->lock);
+  g_boxed_free (GC_TYPE_SEARCH_CONTEXT, context->criteria);
+
+  G_OBJECT_CLASS (gc_search_context_parent_class)->finalize (object);
+}
+
+static void
+gc_search_context_class_init (GcSearchContextClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->set_property = gc_search_context_set_property;
+  object_class->finalize = gc_search_context_finalize;
+
+  search_context_props[SEARCH_CONTEXT_PROP_CRITERIA] =
+    g_param_spec_boxed ("criteria", NULL, NULL,
+                        GC_TYPE_SEARCH_CRITERIA,
+                        G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE);
+  g_object_class_install_properties (object_class, SEARCH_CONTEXT_LAST_PROP,
+                                     search_context_props);
+}
+
+static void
+gc_search_context_init (GcSearchContext *context)
+{
+  g_mutex_init (&context->lock);
+}
+
+GcSearchContext *
+gc_search_context_new (GcSearchCriteria *criteria)
+{
+  return g_object_new (GC_TYPE_SEARCH_CONTEXT, "criteria", criteria, NULL);
+}
+
+static void
+gc_search_context_search_thread (GTask        *task,
+                                 gpointer      source_object,
+                                 gpointer      task_data,
+                                 GCancellable *cancellable)
+{
+  GArray *result;
+  struct SearchData *data = task_data;
+
+  if (g_once_init_enter (&all_blocks_initialized))
+    {
+      uc_all_blocks (&all_blocks, &all_block_count);
+      g_once_init_leave (&all_blocks_initialized, 1);
+    }
+
+  result = g_array_new (FALSE, FALSE, sizeof (gunichar));
+  while (gc_character_iter_next (&data->context->iter))
+    {
+      gunichar uc;
+
+      if (g_task_return_error_if_cancelled (task))
+        {
+          g_mutex_lock (&data->context->lock);
+          data->context->state = GC_SEARCH_STATE_NOT_STARTED;
+          g_mutex_unlock (&data->context->lock);
+          return;
+        }
+
+      if (result->len == data->max_matches)
+        {
+          g_mutex_lock (&data->context->lock);
+          data->context->state = GC_SEARCH_STATE_STEP_FINISHED;
+          g_mutex_unlock (&data->context->lock);
+
+          g_task_return_pointer (task, result, (GDestroyNotify) g_array_unref);
+          return;
+        }
+
+      uc = gc_character_iter_get (&data->context->iter);
+      g_array_append_val (result, uc);
+    }
+
+  g_mutex_lock (&data->context->lock);
+  data->context->state = GC_SEARCH_STATE_FINISHED;
+  g_mutex_unlock (&data->context->lock);
+
   g_task_return_pointer (task, result, (GDestroyNotify) g_array_unref);
 }
 
-/**
- * gc_search_related:
- * @uc: a #gunichar.
- * @max_matches: the maximum number of results.
- * @cancellable: a #GCancellable.
- * @callback: a #GAsyncReadyCallback.
- * @user_data: a user data passed to @callback.
- */
 void
-gc_search_related (gunichar            uc,
-                   gint                max_matches,
-                   GCancellable       *cancellable,
-                   GAsyncReadyCallback callback,
-                   gpointer            user_data)
+gc_search_context_search  (GcSearchContext    *context,
+                           gint                max_matches,
+                           GCancellable       *cancellable,
+                           GAsyncReadyCallback callback,
+                           gpointer            user_data)
 {
   GTask *task;
   struct SearchData *data;
 
-  task = g_task_new (NULL, cancellable, callback, user_data);
+  g_mutex_lock (&context->lock);
+  task = g_task_new (context, cancellable, callback, user_data);
+  switch (context->state)
+    {
+    case GC_SEARCH_STATE_NOT_STARTED:
+      switch (context->criteria->type)
+        {
+        case GC_SEARCH_CRITERIA_CATEGORY:
+          gc_character_iter_init_for_category (&context->iter,
+                                               context->criteria->u.category);
+          break;
+        case GC_SEARCH_CRITERIA_KEYWORDS:
+          gc_character_iter_init_for_keywords (&context->iter,
+                                               (const gchar * const *) context->criteria->u.keywords);
+          break;
+        case GC_SEARCH_CRITERIA_SCRIPTS:
+          gc_character_iter_init_for_scripts (&context->iter,
+                                              context->criteria->u.scripts);
+          break;
+        case GC_SEARCH_CRITERIA_RELATED:
+          gc_character_iter_init_for_related (&context->iter,
+                                              context->criteria->u.related);
+        }
+      context->state = GC_SEARCH_STATE_STEP_STARTED;
+      break;
+
+    case GC_SEARCH_STATE_STEP_STARTED:
+      g_mutex_unlock (&context->lock);
+      g_task_return_new_error (task,
+                               GC_SEARCH_ERROR,
+                               GC_SEARCH_ERROR_INVALID_STATE,
+                               "search step already started");
+      return;
+
+    case GC_SEARCH_STATE_STEP_FINISHED:
+      break;
+
+    case GC_SEARCH_STATE_FINISHED:
+      g_mutex_unlock (&context->lock);
+      g_task_return_new_error (task,
+                               GC_SEARCH_ERROR,
+                               GC_SEARCH_ERROR_INVALID_STATE,
+                               "search context destroyed");
+      return;
+    }
+  g_mutex_unlock (&context->lock);
 
   data = g_slice_new0 (struct SearchData);
-  data->uc = uc;
+  data->context = g_object_ref (context);
   data->max_matches = max_matches;
-  g_task_set_task_data (task, data,
-                        (GDestroyNotify) search_data_free);
-  g_task_run_in_thread (task, gc_search_related_thread);
+  g_task_set_task_data (task, data, (GDestroyNotify) search_data_free);
+  g_task_run_in_thread (task, gc_search_context_search_thread);
 }
 
 /**
- * gc_search_finish:
+ * gc_search_context_search_finish:
+ * @context: a #GcSearchContext.
  * @result: a #GAsyncResult.
  * @error: return location of an error.
  *
  * Returns: (transfer full): an array of characters.
  */
 GcSearchResult *
-gc_search_finish (GAsyncResult *result,
-                  GError      **error)
+gc_search_context_search_finish (GcSearchContext *context,
+                                 GAsyncResult    *result,
+                                 GError         **error)
 {
-  g_return_val_if_fail (g_task_is_valid (result, NULL), NULL);
+  g_return_val_if_fail (g_task_is_valid (result, context), NULL);
 
   return g_task_propagate_pointer (G_TASK (result), error);
 }
 
+gboolean
+gc_search_context_is_finished (GcSearchContext *context)
+{
+  return context->state == GC_SEARCH_STATE_FINISHED;
+}
+
 /**
  * gc_gtk_clipboard_get:
  *
diff --git a/lib/gc.h b/lib/gc.h
index 688c3a3..5a074af 100644
--- a/lib/gc.h
+++ b/lib/gc.h
@@ -35,52 +35,76 @@ typedef enum
 typedef GArray GcSearchResult;
 typedef gboolean (*GcSearchFunc) (gunichar uc, gpointer user_data);
 
-GType           gc_search_result_get_type (void);
-gunichar        gc_search_result_get      (GcSearchResult       *result,
-                                           gint                  index);
-
-void            gc_search_by_category     (GcCategory            category,
-                                           gint                  max_matches,
-                                           GCancellable         *cancellable,
-                                           GAsyncReadyCallback   callback,
-                                           gpointer              user_data);
-void            gc_search_by_keywords     (const gchar * const * keywords,
-                                           gint                  max_matches,
-                                           GCancellable         *cancellable,
-                                           GAsyncReadyCallback   callback,
-                                           gpointer              user_data);
-void            gc_search_by_scripts      (const gchar * const * scripts,
-                                           gint                  max_matches,
-                                           GCancellable         *cancellable,
-                                           GAsyncReadyCallback   callback,
-                                           gpointer              user_data);
-void            gc_search_related         (gunichar              uc,
-                                           gint                  max_matches,
-                                           GCancellable         *cancellable,
-                                           GAsyncReadyCallback   callback,
-                                           gpointer              user_data);
-GcSearchResult *gc_search_finish          (GAsyncResult         *result,
-                                           GError              **error);
-
-gchar          *gc_character_name         (gunichar              uc);
+#define GC_SEARCH_ERROR (gc_search_error_quark ())
+
+typedef enum
+  {
+    GC_SEARCH_ERROR_FAILED,
+    GC_SEARCH_ERROR_INVALID_STATE
+  } GcSearchError;
+
+#define GC_TYPE_SEARCH_CRITERIA (gc_search_criteria_get_type ())
+
+typedef struct _GcSearchCriteria GcSearchCriteria;
+
+#define GC_TYPE_SEARCH_CONTEXT (gc_search_context_get_type ())
+G_DECLARE_FINAL_TYPE (GcSearchContext, gc_search_context,
+                      GC, SEARCH_CONTEXT, GObject)
+
+GType                 gc_search_result_get_type
+                                            (void);
+gunichar              gc_search_result_get  (GcSearchResult       *result,
+                                             gint                  index);
+
+GType                 gc_search_criteria_get_type
+                                            (void);
+
+GcSearchCriteria     *gc_search_criteria_new_category
+                                            (GcCategory            category);
+
+GcSearchCriteria     *gc_search_criteria_new_keywords
+                                            (const gchar * const * keywords);
+
+GcSearchCriteria     *gc_search_criteria_new_scripts
+                                            (const gchar * const * scripts);
+
+GcSearchCriteria     *gc_search_criteria_new_related
+                                            (gunichar              uc);
+
+GcSearchContext      *gc_search_context_new (GcSearchCriteria     *criteria);
+void                  gc_search_context_search
+                                            (GcSearchContext      *context,
+                                             gint                  max_matches,
+                                             GCancellable         *cancellable,
+                                             GAsyncReadyCallback   callback,
+                                             gpointer              user_data);
+GcSearchResult       *gc_search_context_search_finish
+                                            (GcSearchContext      *context,
+                                             GAsyncResult         *result,
+                                             GError              **error);
+gboolean              gc_search_context_is_finished
+                                            (GcSearchContext      *context);
+
+gchar                *gc_character_name     (gunichar              uc);
 
 /* GTK+ support.  gtk_clipboard_get() takes an GdkAtom as the first
    argument, but GdkAtom is not accessible through GI.  */
 
-GtkClipboard   *gc_gtk_clipboard_get      (void);
+GtkClipboard         *gc_gtk_clipboard_get  (void);
 
 /* Pango support.  PangoAttrFallback is not accessible from GI.  */
-void            gc_pango_layout_disable_fallback
-                                          (PangoLayout          *layout);
+void                  gc_pango_layout_disable_fallback
+                                            (PangoLayout          *layout);
 
-gboolean        gc_pango_context_font_has_glyph
-                                          (PangoContext         *context,
-                                           PangoFont            *font,
-                                           gunichar              uc);
+gboolean              gc_pango_context_font_has_glyph
+                                            (PangoContext         *context,
+                                             PangoFont            *font,
+                                             gunichar              uc);
 
-gchar          *gc_get_current_language   (void);
+gchar                *gc_get_current_language
+                                            (void);
 const gchar * const * gc_get_scripts_for_language
-                                          (const gchar          *language);
+                                            (const gchar          *language);
 
 G_END_DECLS
 
diff --git a/src/character.js b/src/character.js
index 7f93af9..8c25102 100644
--- a/src/character.js
+++ b/src/character.js
@@ -115,13 +115,14 @@ const CharacterDialog = new Lang.Class({
 
         this._cancellable.cancel();
         this._cancellable.reset();
-        Gc.search_related(
-            this._character,
+        let criteria = Gc.SearchCriteria.new_related(this._character);
+        let context = new Gc.SearchContext({ criteria: criteria });
+        context.search(
             -1,
             this._cancellable,
-            Lang.bind(this, function(source_object, res, user_data) {
+            Lang.bind(this, function(context, res, user_data) {
                 try {
-                    let result = Gc.search_finish(res);
+                    let result = context.search_finish(res);
                     this._finishSearch(result);
                 } catch (e) {
                     log("Failed to search related: " + e);
diff --git a/src/characterList.js b/src/characterList.js
index 2f79f43..14a1a57 100644
--- a/src/characterList.js
+++ b/src/characterList.js
@@ -270,22 +270,18 @@ const CharacterListView = new Lang.Class({
 
         this._characters = [];
         this._spinnerTimeoutId = 0;
+        this._searchContext = null;
         this._cancellable = new Gio.Cancellable();
+        this._cancellable.connect(Lang.bind(this, function () {
+            this._stopSpinner();
+            this._searchContext = null;
+            this._characters = [];
+            this.updateCharacterList();
+        }));
+        scroll.connect('edge-reached', Lang.bind(this, this._onEdgeReached));
     },
 
-    _stopSpinner: function() {
-        if (this._spinnerTimeoutId > 0) {
-            GLib.source_remove(this._spinnerTimeoutId);
-            this._spinnerTimeoutId = 0;
-            this._loading_spinner.stop();
-        }
-        this._cancellable.reset();
-    },
-
-    _startSearch: function() {
-        this._cancellable.cancel();
-        this._stopSpinner();
-
+    _startSpinner: function() {
         this._spinnerTimeoutId =
             GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000,
                              Lang.bind(this, function () {
@@ -295,6 +291,14 @@ const CharacterListView = new Lang.Class({
                              }));
     },
 
+    _stopSpinner: function() {
+        if (this._spinnerTimeoutId > 0) {
+            GLib.source_remove(this._spinnerTimeoutId);
+            this._spinnerTimeoutId = 0;
+            this._loading_spinner.stop();
+        }
+    },
+
     _finishSearch: function(result) {
         this._stopSpinner();
 
@@ -343,63 +347,66 @@ const CharacterListView = new Lang.Class({
         this.show_all();
     },
 
-    searchByCategory: function(category) {
-        if ('scripts' in category) {
-            this.searchByScripts(category.scripts);
-            return;
+    _onEdgeReached: function(scrolled, pos) {
+        if (pos == Gtk.PositionType.BOTTOM &&
+            this._searchContext != null && !this._searchContext.is_finished()) {
+            this._searchWithContext(this._searchContext, MAX_SEARCH_RESULTS);
+        }
+    },
+
+    _addSearchResult: function(result) {
+        for (let index = 0; index < result.len; index++) {
+            this._characters.push(Gc.search_result_get(result, index));
         }
 
-        this._startSearch()
-        Gc.search_by_category(
-            category.category,
-            -1,
-            this._cancellable,
-            Lang.bind(this,
-                      function(source_object, res, user_data) {
-                          try {
-                              let result = Gc.search_finish(res);
-                              this._finishSearch(result);
-                          } catch (e) {
-                              log("Failed to search by category: " + e);
-                          }
-                      }));
+        this.updateCharacterList()
     },
 
-    searchByKeywords: function(keywords) {
-        this._startSearch()
-        Gc.search_by_keywords(
-            keywords,
-            MAX_SEARCH_RESULTS,
+    _searchWithContext: function(context, count) {
+        this._startSpinner();
+        context.search(
+            count,
             this._cancellable,
-            Lang.bind(this, function(source_object, res, user_data) {
+            Lang.bind(this, function(context, res, user_data) {
+                this._stopSpinner();
                 try {
-                    let result = Gc.search_finish(res);
-                    this._finishSearch(result);
+                    let result = context.search_finish(res);
+                    this._addSearchResult(result);
                 } catch (e) {
-                    log("Failed to search by keywords: " + e);
+                    log("Failed to search: " + e);
                 }
             }));
     },
 
+    searchByCategory: function(category) {
+        if ('scripts' in category) {
+            this.searchByScripts(category.scripts);
+            return;
+        }
+
+        this._characters = [];
+        let criteria = Gc.SearchCriteria.new_category(category.category);
+        this._searchContext = new Gc.SearchContext({ criteria: criteria });
+        this._searchWithContext(this._searchContext, MAX_SEARCH_RESULTS);
+    },
+
+    searchByKeywords: function(keywords) {
+        this._characters = [];
+        let criteria = Gc.SearchCriteria.new_keywords(keywords);
+        this._searchContext = new Gc.SearchContext({ criteria: criteria });
+        this._searchWithContext(this._searchContext, MAX_SEARCH_RESULTS);
+    },
+
     searchByScripts: function(scripts) {
-        this._startSearch()
-        Gc.search_by_scripts(
-            scripts,
-            -1,
-            this._cancellable,
-            Lang.bind(this, function(source_object, res, user_data) {
-                try {
-                    let result = Gc.search_finish(res);
-                    this._finishSearch(result);
-                } catch (e) {
-                    log("Failed to search by scripts: " + e);
-                }
-            }));
+        this._characters = [];
+        var criteria = Gc.SearchCriteria.new_scripts(scripts);
+        this._searchContext = new Gc.SearchContext({ criteria: criteria });
+        this._searchWithContext(this._searchContext, MAX_SEARCH_RESULTS);
     },
 
     cancelSearch: function() {
         this._cancellable.cancel();
-        this._finishSearch([]);
+        this._cancellable.reset();
     },
 
     setFilterFont: function(family) {



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]