[gnome-characters/wip/dueno/emoji] ui: Implement Emoji subcategories
- From: Daiki Ueno <dueno src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-characters/wip/dueno/emoji] ui: Implement Emoji subcategories
- Date: Mon, 21 Aug 2017 14:02:36 +0000 (UTC)
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]