[gnome-characters/wip/dueno/emoji] ui: Implement Emoji subcategories



commit e0e14db7b4b6406f28a334ecb51d9d8e5a92f43a
Author: Daiki Ueno <dueno src gnome org>
Date:   Fri Aug 18 17:29:58 2017 +0200

    ui: Implement Emoji subcategories

 data/characterlist.ui                        |   46 ----
 data/mainview.ui                             |   53 +++++
 data/mainwindow.ui                           |   18 ++
 data/org.gnome.Characters.data.gresource.xml |    1 +
 lib/gc.c                                     |   80 +++++++-
 lib/gc.h                                     |   19 +-
 src/categoryList.js                          |  286 ++++++++++++++++++++------
 src/character.js                             |    2 +
 src/characterList.js                         |  211 +++++++++++++------
 src/util.js                                  |    9 +
 src/window.js                                |  172 ++++++++++++----
 11 files changed, 676 insertions(+), 221 deletions(-)
---
diff --git a/data/characterlist.ui b/data/characterlist.ui
index 167beb0..8ceb53e 100644
--- a/data/characterlist.ui
+++ b/data/characterlist.ui
@@ -81,51 +81,5 @@
        <property name="name">loading</property>
       </packing>
     </child>
-    <child>
-      <object class="GtkGrid" id="empty-recent-grid">
-       <property name="can_focus">False</property>
-       <property name="visible">False</property>
-       <property name="orientation">vertical</property>
-       <property name="halign">center</property>
-       <property name="valign">center</property>
-       <style>
-         <class name="banner"/>
-       </style>
-       <child>
-         <object class="GtkImage" id="empty-recent-image">
-           <property name="can_focus">False</property>
-           <property name="visible">True</property>
-           <property name="halign">center</property>
-           <property name="pixel_size">128</property>
-           <property name="icon_name">characters-punctuation-symbolic</property>
-         </object>
-       </child>
-       <child>
-         <object class="GtkLabel" id="empty-recent-label">
-           <property name="can_focus">False</property>
-           <property name="visible">True</property>
-           <property name="halign">center</property>
-           <property name="label" translatable="yes">No recent characters found</property>
-           <style>
-             <class name="banner-label"/>
-           </style>
-         </object>
-       </child>
-       <child>
-         <object class="GtkLabel" id="empty-recent-hint">
-           <property name="can_focus">False</property>
-           <property name="visible">True</property>
-           <property name="halign">center</property>
-           <property name="label" translatable="yes">Characters will appear here if you use them.</property>
-           <style>
-             <class name="banner-hints"/>
-           </style>
-         </object>
-       </child>
-      </object>
-      <packing>
-       <property name="name">empty-recent</property>
-      </packing>
-    </child>
   </template>
 </interface>
diff --git a/data/mainview.ui b/data/mainview.ui
new file mode 100644
index 0000000..6ab2f2a
--- /dev/null
+++ b/data/mainview.ui
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="Gjs_MainView" parent="GtkStack">
+    <property name="visible">True</property>
+    <child>
+      <object class="GtkGrid" id="empty-recent-grid">
+       <property name="can_focus">False</property>
+       <property name="visible">False</property>
+       <property name="orientation">vertical</property>
+       <property name="halign">center</property>
+       <property name="valign">center</property>
+       <style>
+         <class name="banner"/>
+       </style>
+       <child>
+         <object class="GtkImage" id="empty-recent-image">
+           <property name="can_focus">False</property>
+           <property name="visible">True</property>
+           <property name="halign">center</property>
+           <property name="pixel_size">128</property>
+           <property name="icon_name">characters-punctuation-symbolic</property>
+         </object>
+       </child>
+       <child>
+         <object class="GtkLabel" id="empty-recent-label">
+           <property name="can_focus">False</property>
+           <property name="visible">True</property>
+           <property name="halign">center</property>
+           <property name="label" translatable="yes">No recent characters found</property>
+           <style>
+             <class name="banner-label"/>
+           </style>
+         </object>
+       </child>
+       <child>
+         <object class="GtkLabel" id="empty-recent-hint">
+           <property name="can_focus">False</property>
+           <property name="visible">True</property>
+           <property name="halign">center</property>
+           <property name="label" translatable="yes">Characters will appear here if you use them.</property>
+           <style>
+             <class name="banner-hints"/>
+           </style>
+         </object>
+       </child>
+      </object>
+      <packing>
+       <property name="name">empty-recent</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/data/mainwindow.ui b/data/mainwindow.ui
index b807448..000414f 100644
--- a/data/mainwindow.ui
+++ b/data/mainwindow.ui
@@ -29,6 +29,24 @@
          </packing>
        </child>
        <child>
+         <object class="GtkButton" id="back-button">
+            <property name="can_focus">True</property>
+            <property name="visible">False</property>
+            <style>
+              <class name="image-button"/>
+            </style>
+            <child>
+              <object class="GtkImage" id="back-button-image">
+               <property name="visible">True</property>
+               <property name="icon-name">go-previous-symbolic</property>
+              </object>
+            </child>
+         </object>
+         <packing>
+            <property name="pack-type">start</property>
+         </packing>
+       </child>
+       <child>
          <object class="GtkMenuButton" id="menu-button">
             <property name="can_focus">False</property>
             <property name="visible">True</property>
diff --git a/data/org.gnome.Characters.data.gresource.xml b/data/org.gnome.Characters.data.gresource.xml
index 40873b3..627866f 100644
--- a/data/org.gnome.Characters.data.gresource.xml
+++ b/data/org.gnome.Characters.data.gresource.xml
@@ -2,6 +2,7 @@
 <gresources>
   <gresource prefix="/org/gnome/Characters">
     <file preprocess="xml-stripblanks">app-menu.ui</file>
+    <file preprocess="xml-stripblanks">mainview.ui</file>
     <file preprocess="xml-stripblanks">mainwindow.ui</file>
     <file preprocess="xml-stripblanks">character.ui</file>
     <file preprocess="xml-stripblanks">characterlist.ui</file>
diff --git a/lib/gc.c b/lib/gc.c
index eb0df99..46bb0df 100644
--- a/lib/gc.c
+++ b/lib/gc.c
@@ -271,11 +271,11 @@ gc_character_iter_init_for_category (GcCharacterIter *iter,
     case GC_CATEGORY_NONE:
       break;
 
-    case GC_CATEGORY_PUNCTUATION:
+    case GC_CATEGORY_LETTER_PUNCTUATION:
       gc_character_iter_init_for_general_category (iter, UC_CATEGORY_P);
       return;
 
-    case GC_CATEGORY_ARROW:
+    case GC_CATEGORY_LETTER_ARROW:
       {
         static uc_block_t arrow_blocks[3];
         static gsize arrow_blocks_size = 0;
@@ -307,14 +307,14 @@ gc_character_iter_init_for_category (GcCharacterIter *iter,
         return;
       }
 
-    case GC_CATEGORY_BULLET:
+    case GC_CATEGORY_LETTER_BULLET:
       gc_character_iter_init (iter);
       iter->characters = bullet_characters;
       iter->character_count = bullet_character_count;
       iter->filter = filter_all;
       return;
 
-    case GC_CATEGORY_PICTURE:
+    case GC_CATEGORY_LETTER_PICTURE:
       {
         static uc_block_t picture_blocks[6];
         static gsize picture_blocks_size = 0;
@@ -362,15 +362,15 @@ gc_character_iter_init_for_category (GcCharacterIter *iter,
       }
       break;
 
-    case GC_CATEGORY_CURRENCY:
+    case GC_CATEGORY_LETTER_CURRENCY:
       gc_character_iter_init_for_general_category (iter, UC_CATEGORY_Sc);
       return;
 
-    case GC_CATEGORY_MATH:
+    case GC_CATEGORY_LETTER_MATH:
       gc_character_iter_init_for_general_category (iter, UC_CATEGORY_Sm);
       return;
 
-    case GC_CATEGORY_LATIN:
+    case GC_CATEGORY_LETTER_LATIN:
       {
         static const uc_script_t *latin_scripts[2];
         latin_scripts[0] = uc_script ('A');
@@ -1157,6 +1157,72 @@ gc_search_context_is_finished (GcSearchContext *context)
   return context->state == GC_SEARCH_STATE_FINISHED;
 }
 
+static int
+filter_compare (const void *a, const void *b)
+{
+  const uint32_t *ac = a, *bc = b;
+  return *ac == *bc ? 0 : (*ac < *bc ? -1 : 1);
+}
+
+/**
+ * gc_filter_characters:
+ * @category: a #GcCategory.
+ * @characters: (array zero-terminated=1) (element-type utf8): an array of characters
+ *
+ * Returns: (transfer full): an array of characters.
+ */
+GcSearchResult *
+gc_filter_characters (GcCategory           category,
+                      const gchar * const *characters)
+{
+  static const struct {
+    const uint32_t *table;
+    size_t length;
+  } emoji_tables[] = {
+    { emoji_smileys_characters, EMOJI_SMILEYS_CHARACTER_COUNT },
+    { emoji_animals_characters, EMOJI_ANIMALS_CHARACTER_COUNT },
+    { emoji_food_characters, EMOJI_FOOD_CHARACTER_COUNT },
+    { emoji_travel_characters, EMOJI_TRAVEL_CHARACTER_COUNT },
+    { emoji_activities_characters, EMOJI_ACTIVITIES_CHARACTER_COUNT },
+    { emoji_objects_characters, EMOJI_OBJECTS_CHARACTER_COUNT },
+    { emoji_symbols_characters, EMOJI_SYMBOLS_CHARACTER_COUNT },
+    { emoji_flags_characters, EMOJI_FLAGS_CHARACTER_COUNT }
+  };
+  GArray *result;
+  size_t i, j;
+
+  result = g_array_new (FALSE, FALSE, sizeof (gunichar));
+
+  g_return_val_if_fail (category == GC_CATEGORY_LETTER || category == GC_CATEGORY_EMOJI, result);
+
+  for (i = 0; characters[i] != 0; i++)
+    {
+      const uint8_t *utf8 = characters[i];
+      size_t utf8_length = u8_strmblen (utf8);
+      uint32_t uc;
+      size_t uc_length = 1;
+
+      u8_to_u32 (utf8, utf8_length, &uc, &uc_length);
+      for (j = 0; j < G_N_ELEMENTS(emoji_tables); j++)
+       {
+         uint32_t *res;
+         res = bsearch (&uc, emoji_tables[j].table, emoji_tables[j].length,
+                        sizeof (uint32_t),
+                        filter_compare);
+         if (res)
+           {
+             if (category == GC_CATEGORY_EMOJI)
+               g_array_append_val (result, uc);
+             break;
+           }
+       }
+      if (j == G_N_ELEMENTS(emoji_tables) && category == GC_CATEGORY_LETTER)
+       g_array_append_val (result, uc);
+    }
+
+  return result;
+}
+
 /**
  * gc_gtk_clipboard_get:
  *
diff --git a/lib/gc.h b/lib/gc.h
index 417f3f9..c575b09 100644
--- a/lib/gc.h
+++ b/lib/gc.h
@@ -18,13 +18,15 @@ G_BEGIN_DECLS
 typedef enum
 {
   GC_CATEGORY_NONE,
-  GC_CATEGORY_PUNCTUATION,
-  GC_CATEGORY_ARROW,
-  GC_CATEGORY_BULLET,
-  GC_CATEGORY_PICTURE,
-  GC_CATEGORY_CURRENCY,
-  GC_CATEGORY_MATH,
-  GC_CATEGORY_LATIN,
+  GC_CATEGORY_LETTER,
+  GC_CATEGORY_LETTER_PUNCTUATION,
+  GC_CATEGORY_LETTER_ARROW,
+  GC_CATEGORY_LETTER_BULLET,
+  GC_CATEGORY_LETTER_PICTURE,
+  GC_CATEGORY_LETTER_CURRENCY,
+  GC_CATEGORY_LETTER_MATH,
+  GC_CATEGORY_LETTER_LATIN,
+  GC_CATEGORY_EMOJI,
   GC_CATEGORY_EMOJI_SMILEYS,
   GC_CATEGORY_EMOJI_ANIMALS,
   GC_CATEGORY_EMOJI_FOOD,
@@ -100,6 +102,9 @@ GcSearchResult       *gc_search_context_search_finish
 gboolean              gc_search_context_is_finished
                                             (GcSearchContext      *context);
 
+GcSearchResult       *gc_filter_characters  (GcCategory            category,
+                                            const gchar * const * characters);
+
 gchar                *gc_character_name     (gunichar              uc);
 gboolean              gc_character_is_invisible
                                             (gunichar              uc);
diff --git a/src/categoryList.js b/src/categoryList.js
index fbb5833..efd548c 100644
--- a/src/categoryList.js
+++ b/src/categoryList.js
@@ -1,6 +1,6 @@
 // -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
 //
-// Copyright (C) 2014-2015  Daiki Ueno <dueno src gnome org>
+// Copyright (C) 2014-2017  Daiki Ueno <dueno src gnome org>
 //
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
@@ -26,60 +26,131 @@ const Gettext = imports.gettext;
 const Gc = imports.gi.Gc;
 const Util = imports.util;
 
-const BaseCategoryList = [
+const CategoryList = [
     {
-        name: 'recent',
-        category: Gc.Category.NONE,
-        title: N_('Recently Used'),
-        icon_name: 'document-open-recent-symbolic'
+        name: 'letters',
+        category: Gc.Category.LETTER,
+        title: N_('Letters & Symbols'),
+        icon_name: 'characters-latin-symbolic',
+        action_name: 'category'
     },
     {
+        name: 'emojis',
+        category: Gc.Category.EMOJI,
+        title: N_('Emojis'),
+        icon_name: 'characters-emoji-smileys',
+        action_name: 'category'
+    }
+];
+
+const LetterCategoryList = [
+    {
         name: 'punctuation',
-        category: Gc.Category.PUNCTUATION,
+        category: Gc.Category.LETTER_PUNCTUATION,
         title: N_('Punctuation'),
-        icon_name: 'characters-punctuation-symbolic'
+        icon_name: 'characters-punctuation-symbolic',
+        action_name: 'subcategory'
     },
     {
         name: 'arrow',
-        category: Gc.Category.ARROW,
+        category: Gc.Category.LETTER_ARROW,
         title: N_('Arrows'),
-        icon_name: 'characters-arrow-symbolic'
+        icon_name: 'characters-arrow-symbolic',
+        action_name: 'subcategory'
     },
     {
         name: 'bullet',
-        category: Gc.Category.BULLET,
+        category: Gc.Category.LETTER_BULLET,
         title: N_('Bullets'),
-        icon_name: 'characters-bullet-symbolic'
+        icon_name: 'characters-bullet-symbolic',
+        action_name: 'subcategory'
     },
     {
         name: 'picture',
-        category: Gc.Category.PICTURE,
+        category: Gc.Category.LETTER_PICTURE,
         title: N_('Pictures'),
-        icon_name: 'characters-picture-symbolic'
+        icon_name: 'characters-picture-symbolic',
+        action_name: 'subcategory'
     },
     {
         name: 'currency',
-        category: Gc.Category.CURRENCY,
+        category: Gc.Category.LETTER_CURRENCY,
         title: N_('Currencies'),
-        icon_name: 'characters-currency-symbolic'
+        icon_name: 'characters-currency-symbolic',
+        action_name: 'subcategory'
     },
     {
         name: 'math',
-        category: Gc.Category.MATH,
+        category: Gc.Category.LETTER_MATH,
         title: N_('Math'),
-        icon_name: 'characters-math-symbolic'
+        icon_name: 'characters-math-symbolic',
+        action_name: 'subcategory'
     },
     {
         name: 'letters',
-        category: Gc.Category.LATIN,
+        category: Gc.Category.LETTER_LATIN,
         title: N_('Letters'),
-        icon_name: 'characters-latin-symbolic'
+        icon_name: 'characters-latin-symbolic',
+        action_name: 'subcategory'
+    }
+];
+
+const EmojiCategoryList = [
+    {
+        name: 'emoji-smileys',
+        category: Gc.Category.EMOJI_SMILEYS,
+        title: N_('Smileys & People'),
+        icon_name: 'characters-emoji-smileys',
+        action_name: 'subcategory'
+    },
+    {
+        name: 'emoji-animals',
+        category: Gc.Category.EMOJI_ANIMALS,
+        title: N_('Animals & Nature'),
+        icon_name: 'characters-emoji-animals',
+        action_name: 'subcategory'
+    },
+    {
+        name: 'emoji-food',
+        category: Gc.Category.EMOJI_FOOD,
+        title: N_('Food & Drink'),
+        icon_name: 'characters-emoji-food',
+        action_name: 'subcategory'
     },
     {
-        name: 'emoticon',
-        category: Gc.Category.EMOTICON,
-        title: N_('Emoticons'),
-        icon_name: 'face-smile-symbolic'
+        name: 'emoji-activities',
+        category: Gc.Category.EMOJI_ACTIVITIES,
+        title: N_('Activities'),
+        icon_name: 'characters-emoji-activities',
+        action_name: 'subcategory'
+    },
+    {
+        name: 'emoji-travel',
+        category: Gc.Category.EMOJI_TRAVEL,
+        title: N_('Travel & Places'),
+        icon_name: 'characters-emoji-travel',
+        action_name: 'subcategory'
+    },
+    {
+        name: 'emoji-objects',
+        category: Gc.Category.EMOJI_OBJECTS,
+        title: N_('Objects'),
+        icon_name: 'characters-emoji-objects',
+        action_name: 'subcategory'
+    },
+    {
+        name: 'emoji-symbols',
+        category: Gc.Category.EMOJI_SYMBOLS,
+        title: N_('Symbols'),
+        icon_name: 'characters-emoji-symbols',
+        action_name: 'subcategory'
+    },
+    {
+        name: 'emoji-flags',
+        category: Gc.Category.EMOJI_FLAGS,
+        title: N_('Flags'),
+        icon_name: 'characters-emoji-flags',
+        action_name: 'subcategory'
     }
 ];
 
@@ -106,20 +177,29 @@ const CategoryListRowWidget = new Lang.Class({
                                     halign: Gtk.Align.START });
         label.get_style_context().add_class('category-label');
         hbox.pack_start(label, true, true, 0);
+
+        if (category.secondary_icon_name) {
+            let icon = new Gio.ThemedIcon({ name: category.secondary_icon_name });
+            let image = Gtk.Image.new_from_gicon(icon, Gtk.IconSize.BUTTON);
+            image.get_style_context().add_class('category-icon');
+            hbox.pack_start(image, false, false, 2);
+        }
     }
 });
 
-var CategoryListWidget = new Lang.Class({
+const CategoryListWidget = new Lang.Class({
     Name: 'CategoryListWidget',
     Extends: Gtk.ListBox,
 
     _init: function(params) {
+        let filtered = Params.filter(params, { categoryList: null });
         params = Params.fill(params, {});
         this.parent(params);
 
         this.get_style_context().add_class('categories');
 
-        this._ensureCategoryList();
+        this._categoryList = filtered.categoryList;
+        this.populateCategoryList();
 
         for (let index in this._categoryList) {
             let category = this._categoryList[index];
@@ -130,13 +210,34 @@ var CategoryListWidget = new Lang.Class({
     },
 
     vfunc_row_selected: function(row) {
-        if (row != null) {
+        if (row != null && row.selectable) {
             let toplevel = row.get_toplevel();
-            let category = toplevel.lookup_action('category');
-            category.activate(new GLib.Variant('s', row.category.name));
+            let action = toplevel.lookup_action(row.category.action_name);
+            action.activate(new GLib.Variant('s', row.category.name));
         }
     },
 
+    populateCategoryList: function() {
+    },
+
+    getCategoryList: function() {
+        return this._categoryList;
+    },
+
+    getCategory: function(name) {
+        for (let index in this._categoryList) {
+            let category = this._categoryList[index];
+            if (category.name == name)
+                return category;
+        }
+        return null;
+    }
+});
+
+const LetterCategoryListWidget = new Lang.Class({
+    Name: 'LetterCategoryListWidget',
+    Extends: CategoryListWidget,
+
     _finishListEngines: function(sources, bus, res) {
         try {
             let engines = bus.list_engines_async_finish(res);
@@ -228,7 +329,22 @@ var CategoryListWidget = new Lang.Class({
         category.scripts = allScripts;
     },
 
-    _buildScriptList: function() {
+    populateCategoryList: function() {
+        // Populate the "scripts" element of the "Letter" category
+        // object, based on the current locale and the input-sources
+        // settings.
+        //
+        // This works asynchronously, in the following call flow:
+        //
+        // _buildScriptList()
+        //    if an IBus input-source is configured:
+        //       _ensureIBusLanguageList()
+        //          ibus_bus_list_engines_async()
+        //             _finishListEngines()
+        //                _finishBuildScriptList()
+        //    else:
+        //       _finishBuildScriptList()
+        //
         let settings =
             Util.getSettings('org.gnome.desktop.input-sources',
                              '/org/gnome/desktop/input-sources/');
@@ -242,42 +358,94 @@ var CategoryListWidget = new Lang.Class({
             else
                 this._finishBuildScriptList(sources);
         }
-    },
+    }
+});
 
-    _ensureCategoryList: function() {
-        if (this._categoryList != null)
-            return;
+const EmojiCategoryListWidget = new Lang.Class({
+    Name: 'EmojiCategoryListWidget',
+    Extends: CategoryListWidget,
 
-        this._categoryList = BaseCategoryList.slice();
+    _init: function(params) {
+        params = Params.fill(params, {});
+        this.parent(params);
 
-        // Populate the "scripts" element of the "Letter" category
-        // object, based on the current locale and the input-sources
-        // settings.
-        //
-        // This works asynchronously, in the following call flow:
-        //
-        // _buildScriptList()
-        //    if an IBus input-source is configured:
-        //       _ensureIBusLanguageList()
-        //          ibus_bus_list_engines_async()
-        //             _finishListEngines()
-        //                _finishBuildScriptList()
-        //    else:
-        //       _finishBuildScriptList()
-        //
-        this._buildScriptList();
-    },
+        let category;
+        let rowWidget;
 
-    getCategoryList: function() {
-        return this._categoryList;
+        category = {
+            name: 'recent',
+            category: Gc.Category.NONE,
+            title: N_('Recently Used'),
+            icon_name: 'document-open-recent-symbolic',
+            action_name: 'subcategory'
+        };
+        rowWidget = new CategoryListRowWidget({}, category);
+        rowWidget.get_style_context().add_class('category');
+        this.prepend(rowWidget);
+        this._recentCategory = category;
+
+        category = {
+            name: 'letters',
+            category: Gc.Category.NONE,
+            title: N_('Letters & Symbols'),
+            icon_name: 'characters-latin-symbolic',
+            secondary_icon_name: 'go-next-symbolic',
+            action_name: 'category',
+        };
+        rowWidget = new CategoryListRowWidget({}, category);
+        rowWidget.get_style_context().add_class('category');
+        let separator = new Gtk.Separator();
+        let separatorRowWidget = new Gtk.ListBoxRow({ selectable: false });
+        separatorRowWidget.add(separator);
+        this.add(separatorRowWidget);
+        this.add(rowWidget);
     },
 
     getCategory: function(name) {
-        for (let index in this._categoryList) {
-            let category = this._categoryList[index];
-            if (category.name == name)
-                return category;
+        if (name == 'recent')
+            return this._recentCategory;
+        return this.parent(name);
+    }
+});
+
+var CategoryListView = new Lang.Class({
+    Name: 'CategoryListView',
+    Extends: Gtk.Stack,
+
+    _init: function(params) {
+        params = Params.fill(params, {
+            hexpand: true, vexpand: true,
+            transition_type: Gtk.StackTransitionType.SLIDE_RIGHT
+        });
+        this.parent(params);
+
+        let emojiCategoryList = new EmojiCategoryListWidget({
+            categoryList: EmojiCategoryList
+        });
+        this.add_named(emojiCategoryList, 'emojis');
+
+        let letterCategoryList = new LetterCategoryListWidget({
+            categoryList: LetterCategoryList
+        });
+        this.add_named(letterCategoryList, 'letters');
+
+        this.set_visible_child_name('emojis');
+
+        this._categoryList = CategoryList.slice();
+
+        this.connect('notify::visible-child-name',
+                     Lang.bind(this, this._ensureTransitionType));
+    },
+
+    _ensureTransitionType: function() {
+        if (this.get_visible_child_name() == 'emojis') {
+            this.transition_type = Gtk.StackTransitionType.SLIDE_RIGHT;
+        } else {
+            this.transition_type = Gtk.StackTransitionType.SLIDE_LEFT;
         }
-        return null;
+    },
+
+    getCategoryList: function() {
+        return this._categoryList;
     }
 });
diff --git a/src/character.js b/src/character.js
index a7832d1..54d7514 100644
--- a/src/character.js
+++ b/src/character.js
@@ -68,6 +68,8 @@ var CharacterDialog = new Lang.Class({
 
         this._fontDescription = filtered.fontDescription;
         this._setCharacter(filtered.character);
+
+        this._copyRevealerTimeoutId = 0;
     },
 
     _finishSearch: function(result) {
diff --git a/src/characterList.js b/src/characterList.js
index 844b299..51954d9 100644
--- a/src/characterList.js
+++ b/src/characterList.js
@@ -197,7 +197,10 @@ const CharacterListWidget = new Lang.Class({
     },
 
     _init: function(params) {
-        let filtered = Params.filter(params, { fontDescription: null });
+        let filtered = Params.filter(params, {
+            fontDescription: null,
+            numRows: NUM_ROWS
+        });
         params = Params.fill(params, {});
         this.parent(params);
         let context = this.get_style_context();
@@ -205,6 +208,7 @@ const CharacterListWidget = new Lang.Class({
         context.save();
         this._cellsPerRow = CELLS_PER_ROW;
         this._fontDescription = filtered.fontDescription;
+        this._numRows = filtered.numRows;
         this._characters = [];
         this._rows = [];
         this.add_events(Gdk.EventMask.BUTTON_PRESS_MASK |
@@ -264,7 +268,7 @@ const CharacterListWidget = new Lang.Class({
     },
 
     vfunc_get_preferred_height_for_width: function(width) {
-        let height = Math.max(this._rows.length, NUM_ROWS) *
+        let height = Math.max(this._rows.length, this._numRows) *
             getCellSize(this._fontDescription);
         return [height, height];
     },
@@ -360,17 +364,18 @@ const CharacterListWidget = new Lang.Class({
 
 const MAX_SEARCH_RESULTS = 100;
 
-var CharacterListView = new Lang.Class({
-    Name: 'CharacterListView',
-    Extends: Gtk.Stack,
-    Template: 'resource:///org/gnome/Characters/characterlist.ui',
-    InternalChildren: ['loading-spinner'],
+var FontFilter = new Lang.Class({
+    Name: 'FontFilter',
+    Extends: GObject.Object,
     Properties: {
         'font': GObject.ParamSpec.string(
             'font', '', '',
             GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
             'Cantarell 50')
     },
+    Signals: {
+        'filter-set': { param_types: [] }
+    },
 
     get font() {
         return this._font;
@@ -387,24 +392,90 @@ var CharacterListView = new Lang.Class({
 
         this._font = v;
         this._fontDescription = fontDescription;
-        if (this.mapped) {
-            this.setCharacters(this._characters);
-            this.show_all();
+    },
+
+    get fontDescription() {
+        if (this._filterFontDescription)
+            return this._filterFontDescription;
+        return this._fontDescription;
+    },
+
+    _init: function(params) {
+        params = Params.fill(params, {});
+        this.parent(params);
+
+        this._fontDescription = null;
+        this._filterFontDescription = null;
+
+        Main.settings.bind('font', this, 'font', Gio.SettingsBindFlags.DEFAULT);
+    },
+
+    setFilterFont: function(v) {
+        let fontDescription;
+        if (v == null) {
+            fontDescription = null;
+        } else {
+            fontDescription = Pango.FontDescription.from_string(v);
+            fontDescription.set_size(this._fontDescription.get_size());
         }
+
+        if ((this._filterFontDescription != null && fontDescription == null) ||
+            (this._filterFontDescription == null && fontDescription != null) ||
+            (this._filterFontDescription != null && fontDescription != null &&
+             !fontDescription.equal(this._filterFontDescription))) {
+            this._filterFontDescription = fontDescription;
+            this.emit('filter-set');
+        }
+    },
+
+    apply: function(widget, characters) {
+        let fontDescription = this._fontDescription;
+        if (this._filterFontDescription) {
+            let context = widget.get_pango_context();
+            let filterFont = context.load_font(this._filterFontDescription);
+            let filteredCharacters = [];
+            for (let index = 0; index < characters.length; index++) {
+                let uc = characters[index];
+                if (Gc.pango_context_font_has_glyph(context, filterFont, uc))
+                    filteredCharacters.push(uc);
+            }
+            characters = filteredCharacters;
+            fontDescription = this._filterFontDescription;
+        }
+
+        return [fontDescription, characters];
+    },
+});
+
+var CharacterListView = new Lang.Class({
+    Name: 'CharacterListView',
+    Extends: Gtk.Stack,
+    Template: 'resource:///org/gnome/Characters/characterlist.ui',
+    InternalChildren: ['loading-spinner'],
+    Signals: {
+        'character-selected': { param_types: [ GObject.TYPE_STRING ] }
     },
 
     _init: function(params) {
+        let filtered = Params.filter(params, {
+            fontFilter: null
+        });
         params = Params.fill(params, {
             hexpand: true, vexpand: true,
             transition_type: Gtk.StackTransitionType.CROSSFADE
         });
         this.parent(params);
 
-        Main.settings.bind('font', this, 'font', Gio.SettingsBindFlags.DEFAULT);
-
-        this._characterList = new CharacterListWidget({ hexpand: true,
-                                                        vexpand: true,
-                                                        fontDescription: this._fontDescription });
+        this._fontFilter = filtered.fontFilter;
+        this._characterList = new CharacterListWidget({
+            hexpand: true,
+            vexpand: true,
+            fontDescription: this._fontFilter.fontDescription
+        });
+        this._characterList.connect('character-selected',
+                                    Lang.bind(this, function(w, c) {
+                                        this.emit('character-selected', c);
+                                    }));
         let scroll = new Gtk.ScrolledWindow({
             hscrollbar_policy: Gtk.PolicyType.NEVER,
             visible: true
@@ -416,6 +487,9 @@ var CharacterListView = new Lang.Class({
         this.add_named(scroll, 'character-list');
         this.visible_child_name = 'character-list';
 
+        this._fontFilter.connect('filter-set',
+                                 Lang.bind(this, this._updateCharacterList));
+
         this._characters = [];
         this._spinnerTimeoutId = 0;
         this._searchContext = null;
@@ -424,7 +498,7 @@ var CharacterListView = new Lang.Class({
             this._stopSpinner();
             this._searchContext = null;
             this._characters = [];
-            this.updateCharacterList();
+            this._updateCharacterList();
         }));
         scroll.connect('edge-reached', Lang.bind(this, this._onEdgeReached));
     },
@@ -451,41 +525,18 @@ var CharacterListView = new Lang.Class({
     _finishSearch: function(result) {
         this._stopSpinner();
 
-        let characters = [];
-        for (let index = 0; index < result.len; index++) {
-            characters.push(Gc.search_result_get(result, index));
-        }
+        let characters = Util.searchResultToArray(result);
 
-        this._characters = characters;
-        this.updateCharacterList()
+        this.setCharacters(characters);
     },
 
     setCharacters: function(characters) {
         this._characters = characters;
+        this._updateCharacterList();
     },
 
-    getFontDescription: function() {
-        if (this._filterFontDescription)
-            return this._filterFontDescription;
-        return this._fontDescription;
-    },
-
-    updateCharacterList: function() {
-        let characters = this._characters;
-        let fontDescription = this._fontDescription;
-        if (this._filterFontDescription) {
-            let context = this.get_pango_context();
-            let filterFont = context.load_font(this._filterFontDescription);
-            let filteredCharacters = [];
-            for (let index = 0; index < characters.length; index++) {
-                let uc = characters[index];
-                if (Gc.pango_context_font_has_glyph(context, filterFont, uc))
-                    filteredCharacters.push(uc);
-            }
-            characters = filteredCharacters;
-            fontDescription = this._filterFontDescription;
-        }
-
+    _updateCharacterList: function() {
+        let [fontDescription, characters] = this._fontFilter.apply(this, this._characters);
         this._characterList.setFontDescription(fontDescription);
         this._characterList.setCharacters(characters);
         if (characters.length == 0) {
@@ -516,7 +567,7 @@ var CharacterListView = new Lang.Class({
         // Sometimes more MAX_SEARCH_RESULTS are visible on screen
         // (eg. fullscreen at 1080p).  We always present a over-full screen,
         // otherwise the lazy loading gets broken
-        let cellSize = getCellSize(this._fontDescription);
+        let cellSize = getCellSize(this._fontFilter.fontDescription);
         let cellsPerRow = Math.floor(allocation.width / cellSize);
         // Ensure the rows cause a scroll
         let heightInRows = Math.ceil((allocation.height + 1) / cellSize);
@@ -533,11 +584,8 @@ var CharacterListView = new Lang.Class({
     },
 
     _addSearchResult: function(result) {
-        for (let index = 0; index < result.len; index++) {
-            this._characters.push(Gc.search_result_get(result, index));
-        }
-
-        this.updateCharacterList()
+        let characters = Util.searchResultToArray(result);
+        this.setCharacters(characters);
     },
 
     _searchWithContext: function(context, count) {
@@ -585,23 +633,56 @@ var CharacterListView = new Lang.Class({
     cancelSearch: function() {
         this._cancellable.cancel();
         this._cancellable.reset();
+    }
+});
+
+var RecentCharacterListView = new Lang.Class({
+    Name: 'RecentCharacterListView',
+    Extends: Gtk.Bin,
+    Signals: {
+        'character-selected': { param_types: [ GObject.TYPE_STRING ] }
     },
 
-    setFilterFont: function(family) {
-        let fontDescription;
-        if (family == null) {
-            fontDescription = null;
-        } else {
-            fontDescription = Pango.FontDescription.from_string(family);
-            fontDescription.set_size(this._fontDescription.get_size());
-        }
+    _init: function(params) {
+        let filtered = Params.filter(params, {
+            category: null,
+            fontFilter: null
+        });
+        params = Params.fill(params, {
+            hexpand: true, vexpand: true
+        });
+        this.parent(params);
 
-        if ((this._filterFontDescription != null && fontDescription == null) ||
-            (this._filterFontDescription == null && fontDescription != null) ||
-            (this._filterFontDescription != null && fontDescription != null &&
-             !fontDescription.equal(this._filterFontDescription))) {
-            this._filterFontDescription = fontDescription;
-            this.updateCharacterList();
-        }
+        this._fontFilter = filtered.fontFilter;
+        this._characterList = new CharacterListWidget({
+            hexpand: true,
+            vexpand: true,
+            fontDescription: this._fontFilter.fontDescription,
+            numRows: 0
+        });
+        this._characterList.connect('character-selected',
+                                    Lang.bind(this, function(w, c) {
+                                        this.emit('character-selected', c);
+                                    }));
+        this.add(this._characterList);
+
+        this._fontFilter.connect('filter-set',
+                                 Lang.bind(this, this._updateCharacterList));
+
+        this._category = filtered.category;
+        this._characters = [];
+    },
+
+    setCharacters: function(characters) {
+        let result = Gc.filter_characters(this._category, characters);
+        this._characters = Util.searchResultToArray(result);
+        this._updateCharacterList();
+    },
+
+    _updateCharacterList: function() {
+        let [fontDescription, characters] = this._fontFilter.apply(this, this._characters);
+        this._characterList.setFontDescription(fontDescription);
+        this._characterList.setCharacters(characters);
+        this.show_all();
     }
 });
diff --git a/src/util.js b/src/util.js
index 2c17482..af362bd 100644
--- a/src/util.js
+++ b/src/util.js
@@ -28,6 +28,7 @@ const Gdk = imports.gi.Gdk;
 const Gio = imports.gi.Gio;
 const GObject = imports.gi.GObject;
 const Gtk = imports.gi.Gtk;
+const Gc = imports.gi.Gc;
 const Lang = imports.lang;
 const Params = imports.params;
 const System = imports.system;
@@ -155,3 +156,11 @@ function toCodePoint(s) {
 
     return codePoint;
 }
+
+function searchResultToArray(result) {
+    let characters = [];
+    for (let index = 0; index < result.len; index++) {
+        characters.push(Gc.search_result_get(result, index));
+    }
+    return characters;
+}
diff --git a/src/window.js b/src/window.js
index 83acdb4..b9d5659 100644
--- a/src/window.js
+++ b/src/window.js
@@ -24,6 +24,7 @@
 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+const Gc = imports.gi.Gc;
 const Gio = imports.gi.Gio;
 const GLib = imports.gi.GLib;
 const GObject = imports.gi.GObject;
@@ -44,7 +45,7 @@ var MainWindow = new Lang.Class({
     Extends: Gtk.ApplicationWindow,
     Template: 'resource:///org/gnome/Characters/mainwindow.ui',
     InternalChildren: ['main-headerbar', 'search-active-button',
-                       'search-bar', 'search-entry',
+                       'search-bar', 'search-entry', 'back-button',
                        'menu-button',
                        'main-grid', 'main-hbox', 'sidebar-grid'],
     Properties: {
@@ -74,7 +75,11 @@ var MainWindow = new Lang.Class({
                           { name: 'category',
                             activate: this._category,
                             parameter_type: new GLib.VariantType('s'),
-                            state: new GLib.Variant('s', 'punctuation') },
+                            state: new GLib.Variant('s', 'emojis') },
+                          { name: 'subcategory',
+                            activate: this._subcategory,
+                            parameter_type: new GLib.VariantType('s'),
+                            state: new GLib.Variant('s', 'emoji-smileys') },
                           { name: 'character',
                             activate: this._character,
                             parameter_type: new GLib.VariantType('s') },
@@ -96,24 +101,31 @@ var MainWindow = new Lang.Class({
         this._search_entry.connect('search-changed',
                                    Lang.bind(this, this._handleSearchChanged));
 
+        this._back_button.connect('clicked',
+                                  Lang.bind(this, function() {
+                                      let action = this.lookup_action('category');
+                                      action.activate(new GLib.Variant('s', 'emojis'));
+                                  }));
+        this._back_button.bind_property('visible',
+                                        this._search_active_button, 'visible',
+                                        GObject.BindingFlags.SYNC_CREATE |
+                                        GObject.BindingFlags.INVERT_BOOLEAN);
+
         this._menu_popover = new Menu.MenuPopover({});
         this._menu_button.set_popover(this._menu_popover);
 
-        this._categoryList =
-            new CategoryList.CategoryListWidget({ vexpand: true });
+        this._categoryListView =
+            new CategoryList.CategoryListView({ vexpand: true });
         let scroll = new Gtk.ScrolledWindow({
             hscrollbar_policy: Gtk.PolicyType.NEVER,
             hexpand: false,
         });
-        scroll.add(this._categoryList);
+        scroll.add(this._categoryListView);
         this._sidebar_grid.add(scroll);
 
-        this._mainView = new MainView({ categoryList: this._categoryList });
-
-        if (this._mainView.recentCharacters.length == 0) {
-            let row = this._categoryList.get_row_at_index(1);
-            this._categoryList.select_row(row);
-        }
+        this._mainView = new MainView({
+            categoryListView: this._categoryListView
+        });
 
         this._main_hbox.pack_start(this._mainView, true, true, 0);
         this._main_grid.show_all();
@@ -123,6 +135,22 @@ var MainWindow = new Lang.Class({
         this.connect('key-press-event', Lang.bind(this, this._handleKeyPress));
     },
 
+    vfunc_map: function() {
+        this.parent();
+        this._selectFirstSubcategory();
+    },
+
+    // Select the first subcategory which contains at least one character.
+    _selectFirstSubcategory: function() {
+        let categoryList = this._categoryListView.get_visible_child();
+        let index = 0;
+        let row = categoryList.get_row_at_index(index);
+        if (row.category.name == 'recent' &&
+            this._mainView.recentCharacters.length == 0)
+            index++;
+        categoryList.select_row(categoryList.get_row_at_index(index));
+    },
+
     get search_active() {
         return this._searchActive;
     },
@@ -200,13 +228,41 @@ var MainWindow = new Lang.Class({
 
         let [name, length] = v.get_string()
 
-        let category = this._categoryList.getCategory(name);
+        this._categoryListView.set_visible_child_name(name);
+        let categoryList = this._categoryListView.get_visible_child();
+        if (categoryList == null)
+            return;
+
+        this._selectFirstSubcategory();
+        let category = categoryList.get_selected_row().category;
+
+        if (name == 'emojis') {
+            this._back_button.hide();
+        } else {
+            this._back_button.show();
+        }
 
         Util.assertNotEqual(category, null);
         this._mainView.setPage(category);
         this._updateTitle(category.title);
     },
 
+    _subcategory: function(action, v) {
+        this.search_active = false;
+
+        let [name, length] = v.get_string()
+
+        let categoryList = this._categoryListView.get_visible_child();
+        if (categoryList == null)
+            return;
+
+        let category = categoryList.getCategory(name);
+        if (category) {
+            this._mainView.setPage(category);
+            this._updateTitle(category.title);
+        }
+    },
+
     _character: function(action, v) {
         let [uc, length] = v.get_string()
         this._mainView.addToRecent(uc);
@@ -234,6 +290,7 @@ var MainWindow = new Lang.Class({
 const MainView = new Lang.Class({
     Name: 'MainView',
     Extends: Gtk.Stack,
+    Template: 'resource:///org/gnome/Characters/mainview.ui',
     Properties: {
         'max-recent-characters': GObject.ParamSpec.uint(
             'max-recent-characters', '', '',
@@ -258,31 +315,58 @@ const MainView = new Lang.Class({
 
     set filterFontFamily(family) {
         this._filterFontFamily = family;
-        this.visible_child.setFilterFont(family);
+        this._fontFilter.setFilterFont(this._filterFontFamily);
     },
 
     _init: function(params) {
-        let filtered = Params.filter(params, { categoryList: null });
+        let filtered = Params.filter(params, { categoryListView: null });
         params = Params.fill(params, {
             hexpand: true, vexpand: true,
             transition_type: Gtk.StackTransitionType.CROSSFADE
         });
         this.parent(params);
 
+        this._fontFilter = new CharacterList.FontFilter({});
         this._filterFontFamily = null;
         this._characterLists = {};
-        this._categoryList = filtered.categoryList;
+        this._recentCharacterLists = {};
+        this._categoryListView = filtered.categoryListView;
 
         let characterList;
-        let categories = this._categoryList.getCategoryList();
-        for (let index in categories) {
-            let category = categories[index];
-            characterList = this._createCharacterList(
-                category.name, _('%s Character List').format(category.title));
-            // FIXME: Can't use GtkContainer.child_get_property.
-            characterList.title = category.title;
-            this.add_titled(characterList, category.name, category.title);
+        let categories = this._categoryListView.getCategoryList();
+        let recentBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL,
+                                      hexpand: true, vexpand: true });
+
+        for (let i in categories) {
+            let category = categories[i];
+            let categoryList = this._categoryListView.get_child_by_name(category.name);
+            let subcategories = categoryList.getCategoryList();
+            for (let j in subcategories) {
+                let subcategory = subcategories[j];
+                characterList = this._createCharacterList(
+                    subcategory.name,
+                    _('%s Character List').format(subcategory.title));
+                // FIXME: Can't use GtkContainer.child_get_property.
+                characterList.title = subcategory.title;
+                this.add_titled(characterList, subcategory.name, subcategory.title);
+            }
+            characterList = this._createRecentCharacterList(
+                category.name,
+                _('Recently Used %s Character List').format(category.title),
+                category.category);
+            this._recentCharacterLists[category.name] = characterList;
+            if (i > 0) {
+                let separator = new Gtk.Separator({});
+                recentBox.pack_end(separator, false, false, 0);
+            }
+            recentBox.pack_end(characterList, true, true, 0);
         }
+        let scroll = new Gtk.ScrolledWindow({
+            hscrollbar_policy: Gtk.PolicyType.NEVER,
+            hexpand: false,
+        });
+        scroll.add(recentBox);
+        this.add_titled(scroll, 'recent', 'Recently Used');
 
         characterList = this._createCharacterList(
             'search-result', _('Search Result Character List'));
@@ -300,13 +384,25 @@ const MainView = new Lang.Class({
     },
 
     _createCharacterList: function(name, accessible_name) {
-        let characterList = new CharacterList.CharacterListView({});
+        let characterList = new CharacterList.CharacterListView({
+            fontFilter: this._fontFilter
+        });
         characterList.get_accessible().accessible_name = accessible_name;
+        characterList.connect('character-selected',
+                              Lang.bind(this, this._handleCharacterSelected));
 
-        let scroll = characterList.get_child_by_name('character-list');
-        let widget = scroll.get_child().get_child();
-        widget.connect('character-selected',
-                       Lang.bind(this, this._handleCharacterSelected));
+        this._characterLists[name] = characterList;
+        return characterList;
+    },
+
+    _createRecentCharacterList: function(name, accessible_name, category) {
+        let characterList = new CharacterList.RecentCharacterListView({
+            fontFilter: this._fontFilter,
+            category: category
+        });
+        characterList.get_accessible().accessible_name = accessible_name;
+        characterList.connect('character-selected',
+                              Lang.bind(this, this._handleCharacterSelected));
 
         this._characterLists[name] = characterList;
         return characterList;
@@ -322,21 +418,23 @@ const MainView = new Lang.Class({
     },
 
     setPage: function(category) {
-        let characterList = this.get_child_by_name(category.name);
-        characterList.setFilterFont(this._filterFontFamily);
-
         if (category.name == 'recent') {
             if (this.recentCharacters.length == 0)
-                characterList.visible_child_name = 'empty-recent';
+                this.visible_child_name = 'empty-recent';
             else {
-                characterList.setCharacters(this.recentCharacters);
-                characterList.updateCharacterList();
+                let categories = this._categoryListView.getCategoryList();
+                for (let i in categories) {
+                    let category = categories[i];
+                    let characterList = this._recentCharacterLists[category.name];
+                    characterList.setCharacters(this.recentCharacters);
+                }
+                this.visible_child_name = 'recent';
             }
         } else {
+            let characterList = this.get_child_by_name(category.name);
             characterList.searchByCategory(category);
+            this.visible_child = characterList;
         }
-
-        this.visible_child = characterList;
     },
 
     addToRecent: function(uc) {
@@ -360,7 +458,7 @@ const MainView = new Lang.Class({
             character: uc,
             modal: true,
             transient_for: this.get_toplevel(),
-            fontDescription: this.visible_child.getFontDescription()
+            fontDescription: this._fontFilter.fontDescription
         });
 
         dialog.show();


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