[gnome-characters] Implement filtering by font



commit 1371a764a26b8158168d0de18e724a678b0c9f82
Author: Daiki Ueno <dueno src gnome org>
Date:   Sat Feb 14 20:54:20 2015 +0900

    Implement filtering by font

 data/mainwindow.ui                           |   19 +++++
 data/menu.ui                                 |   49 +++++++++++
 data/org.gnome.Characters.data.gresource.xml |    1 +
 src/characterList.js                         |  113 ++++++++++++++++++--------
 src/menu.js                                  |   92 +++++++++++++++++++++
 src/org.gnome.Characters.src.gresource.xml   |    1 +
 src/window.js                                |   57 +++++++++++---
 7 files changed, 287 insertions(+), 45 deletions(-)
---
diff --git a/data/mainwindow.ui b/data/mainwindow.ui
index 4452d46..e935134 100644
--- a/data/mainwindow.ui
+++ b/data/mainwindow.ui
@@ -26,6 +26,25 @@
             <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>
+           <property name="use_popover">True</property>
+            <style>
+              <class name="image-button"/>
+            </style>
+            <child>
+              <object class="GtkImage" id="menu-button-image">
+               <property name="visible">True</property>
+               <property name="icon-name">open-menu-symbolic</property>
+              </object>
+            </child>
+         </object>
+         <packing>
+            <property name="pack-type">end</property>
+         </packing>
+       </child>
       </object>
     </child>
     <child>
diff --git a/data/menu.ui b/data/menu.ui
new file mode 100644
index 0000000..d6f1f21
--- /dev/null
+++ b/data/menu.ui
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="Gjs_MenuPopover" parent="GtkPopover">
+    <property name="vexpand">True</property>
+    <child>
+      <object class="GtkGrid" id="main-grid">
+       <property name="can_focus">False</property>
+       <property name="visible">True</property>
+       <property name="orientation">vertical</property>
+       <child>
+         <object class="GtkSearchEntry" id="search-entry">
+           <property name="can_focus">True</property>
+           <property name="visible">True</property>
+           <property name="halign">fill</property>
+         </object>
+       </child>
+       <child>
+         <object class="GtkScrolledWindow" id="font-scrolled">
+           <property name="visible">True</property>
+           <property name="can_focus">False</property>
+           <property name="margin_start">6</property>
+           <property name="margin_end">6</property>
+           <property name="margin_top">6</property>
+           <property name="margin_bottom">6</property>
+           <property name="hscrollbar_policy">never</property>
+           <child>
+             <object class="GtkViewport" id="font-viewport">
+               <property name="can_focus">False</property>
+               <property name="visible">True</property>
+               <child>
+                 <object class="GtkListBox" id="font-listbox">
+                   <property name="can_focus">False</property>
+                   <property name="visible">True</property>
+                   <property name="vexpand">True</property>
+                   <property name="selection_mode">none</property>
+                   <child>
+                     <placeholder/>
+                   </child>
+                 </object>
+               </child>
+             </object>
+           </child>
+         </object>
+       </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/org.gnome.Characters.data.gresource.xml b/data/org.gnome.Characters.data.gresource.xml
index 5ad4f21..40873b3 100644
--- a/data/org.gnome.Characters.data.gresource.xml
+++ b/data/org.gnome.Characters.data.gresource.xml
@@ -5,6 +5,7 @@
     <file preprocess="xml-stripblanks">mainwindow.ui</file>
     <file preprocess="xml-stripblanks">character.ui</file>
     <file preprocess="xml-stripblanks">characterlist.ui</file>
+    <file preprocess="xml-stripblanks">menu.ui</file>
     <file>application.css</file>
   </gresource>
 </gresources>
diff --git a/src/characterList.js b/src/characterList.js
index 00eb693..4b7fa80 100644
--- a/src/characterList.js
+++ b/src/characterList.js
@@ -86,43 +86,16 @@ const CharacterListWidget = new Lang.Class({
     Signals: {
         'character-selected': { param_types: [ GObject.TYPE_STRING ] }
     },
-    Properties: {
-        'font': GObject.ParamSpec.string(
-            'font', '', '',
-            GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
-            'Cantarell 50')
-    },
-
-    get font() {
-        return this._font;
-    },
-
-    set font(v) {
-        let fontDescription = Pango.FontDescription.from_string(v);
-        if (fontDescription.get_size() == 0)
-            fontDescription.set_size(CELL_SIZE * Pango.SCALE);
-
-        if (this._fontDescription &&
-            fontDescription.equal(this._fontDescription))
-            return;
-
-        this._font = v;
-        this._fontDescription = fontDescription;
-        if (this._characters) {
-            this.setCharacters(this._characters);
-            this.show_all();
-        }
-    },
 
     _init: function(params) {
+        let filtered = Params.filter(params, { fontDescription: null });
         params = Params.fill(params, {});
         this.parent(params);
         this.get_style_context().add_class('character-list');
         this._cellsPerRow = CELLS_PER_ROW;
-        this._font = null;
-        this._fontDescription = null;
+        this._fontDescription = filtered.fontDescription;
+        this._characters = [];
         this._rows = [];
-        Main.settings.bind('font', this, 'font', Gio.SettingsBindFlags.DEFAULT);
         this.add_events(Gdk.EventMask.BUTTON_PRESS_MASK);
     },
 
@@ -175,6 +148,10 @@ const CharacterListWidget = new Lang.Class({
         return row;
     },
 
+    setFontDescription: function(fontDescription) {
+        this._fontDescription = fontDescription;
+    },
+
     setCharacters: function(characters) {
         this._rows = [];
         this._characters = characters;
@@ -231,19 +208,50 @@ const CharacterListView = new Lang.Class({
     Extends: Gtk.Stack,
     Template: 'resource:///org/gnome/Characters/characterlist.ui',
     InternalChildren: ['loading-banner-spinner'],
+    Properties: {
+        'font': GObject.ParamSpec.string(
+            'font', '', '',
+            GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
+            'Cantarell 50')
+    },
+
+    get font() {
+        return this._font;
+    },
+
+    set font(v) {
+        let fontDescription = Pango.FontDescription.from_string(v);
+        if (fontDescription.get_size() == 0)
+            fontDescription.set_size(CELL_SIZE * Pango.SCALE);
+
+        if (this._fontDescription &&
+            fontDescription.equal(this._fontDescription))
+            return;
+
+        this._font = v;
+        this._fontDescription = fontDescription;
+        if (this.mapped) {
+            this.setCharacters(this._characters);
+            this.show_all();
+        }
+    },
 
     _init: function(params) {
-        let filtered = Params.filter(params, { characterList: null });
         params = Params.fill(params, { hexpand: true, vexpand: true });
         this.parent(params);
 
-        this._characterList = filtered.characterList;
+        Main.settings.bind('font', this, 'font', Gio.SettingsBindFlags.DEFAULT);
+
+        this._characterList = new CharacterListWidget({ hexpand: true,
+                                                        vexpand: true,
+                                                        fontDescription: this._fontDescription });
         let scroll = new Gtk.ScrolledWindow({
             hscrollbar_policy: Gtk.PolicyType.NEVER
         });
         scroll.add(this._characterList);
         this.add_named(scroll, 'character-list');
 
+        this._characters = [];
         this._spinnerTimeoutId = 0;
         this._cancellable = new Gio.Cancellable();
     },
@@ -273,10 +281,31 @@ const CharacterListView = new Lang.Class({
             characters.push(Gc.search_result_get(result, index));
         }
 
-        this.setCharacters(characters)
+        this._characters = characters;
+        this.updateCharacterList()
     },
 
     setCharacters: function(characters) {
+        this._characters = characters;
+    },
+
+    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;
+        }
+
+        this._characterList.setFontDescription(fontDescription);
         this._characterList.setCharacters(characters);
         if (characters.length == 0) {
             this.visible_child_name = 'search-banner';
@@ -322,5 +351,21 @@ const CharacterListView = new Lang.Class({
     cancelSearch: function() {
         this._cancellable.cancel();
         this._finishSearch([]);
-    }
+    },
+
+    setFilterFont: function(family) {
+        if (family == null) {
+            this._filterFontDescription = null;
+            return;
+        }
+
+        let fontDescription = Pango.FontDescription.from_string(family);
+        fontDescription.set_size(this._fontDescription.get_size());
+
+        if (!(this._filterFontDescription &&
+              fontDescription.equal(this._filterFontDescription))) {
+            this._filterFontDescription = fontDescription;
+            this.updateCharacterList();
+        }
+    },
 });
diff --git a/src/menu.js b/src/menu.js
new file mode 100644
index 0000000..8d8a440
--- /dev/null
+++ b/src/menu.js
@@ -0,0 +1,92 @@
+// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
+//
+// Copyright (C) 2015  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
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+const Params = imports.params;
+const Pango = imports.gi.Pango;
+
+const MenuPopover = new Lang.Class({
+    Name: 'MenuPopover',
+    Extends: Gtk.Popover,
+    Template: 'resource:///org/gnome/Characters/menu.ui',
+    InternalChildren: ['search-entry', 'font-listbox'],
+
+    _init: function(params) {
+        params = Params.fill(params, {});
+        this.parent(params);
+
+        let context = this.get_pango_context();
+        let families = context.list_families();
+        families = families.sort(function(a, b) {
+            return a.get_name().localeCompare(b.get_name());
+        });
+        for (let index in families) {
+            let row = new Gtk.ListBoxRow({ visible: true });
+            row._family = families[index];
+            row.add(new Gtk.Label({ label: row._family.get_name(),
+                                   visible: true,
+                                    halign: Gtk.Align.START }));
+            this._font_listbox.add(row);
+        }
+
+        this._keywords = [];
+        this._search_entry.connect('search-changed',
+                                   Lang.bind(this, this._handleSearchChanged));
+        this._font_listbox.connect('row-activated',
+                                   Lang.bind(this, this._handleRowActivated));
+        this._font_listbox.set_filter_func(Lang.bind(this, this._filterFunc));
+
+        // This silents warning at Characters exit about this widget being
+        // visible but not mapped.  Borrowed from Maps.
+        this.connect('unmap', function(popover) { popover.hide(); });
+    },
+
+    _handleSearchChanged: function(entry) {
+        let text = entry.get_text().replace(/^\s+|\s+$/g, '');
+        let keywords = text == '' ? [] : text.split(/\s+/);
+        this._keywords = keywords.map(String.toLowerCase);
+       this._font_listbox.invalidate_filter();
+        return true;
+    },
+
+    _handleRowActivated: function(listBox, row) {
+        if (row != null) {
+            let toplevel = this.get_toplevel();
+            let action = toplevel.lookup_action('filter-font');
+            action.activate(new GLib.Variant('s', row._family.get_name()));
+        }
+    },
+
+    _filterFunc: function(row) {
+        if (this._keywords.length == 0)
+           return true;
+       else {
+            let name = row._family.get_name();
+            let nameWords = name.split(/\s+/).map(String.toLowerCase);
+           return this._keywords.some(function(keyword, index, array) {
+               return nameWords.some(function(nameWord, index, array) {
+                   return nameWord.indexOf(keyword) >= 0;
+               });
+           });
+       }
+    }
+});
diff --git a/src/org.gnome.Characters.src.gresource.xml b/src/org.gnome.Characters.src.gresource.xml
index 57f4fcf..eaf5281 100644
--- a/src/org.gnome.Characters.src.gresource.xml
+++ b/src/org.gnome.Characters.src.gresource.xml
@@ -2,6 +2,7 @@
 <gresources>
   <gresource prefix="/org/gnome/Characters/js">
     <file>main.js</file>
+    <file>menu.js</file>
     <file>params.js</file>
     <file>util.js</file>
     <file>window.js</file>
diff --git a/src/window.js b/src/window.js
index d425d29..080c452 100644
--- a/src/window.js
+++ b/src/window.js
@@ -33,8 +33,7 @@ const Params = imports.params;
 const CategoryList = imports.categoryList;
 const Character = imports.character;
 const CharacterList = imports.characterList;
-const Pango = imports.gi.Pango;
-const Gc = imports.gi.Gc;
+const Menu = imports.menu;
 const Gettext = imports.gettext;
 
 const Main = imports.main;
@@ -46,6 +45,7 @@ const MainWindow = new Lang.Class({
     Template: 'resource:///org/gnome/Characters/mainwindow.ui',
     InternalChildren: ['main-headerbar', 'search-active-button',
                        'search-bar', 'search-entry',
+                       'menu-button',
                        'main-grid', 'main-hbox', 'sidebar-grid'],
     Properties: {
         'search-active': GObject.ParamSpec.boolean(
@@ -75,6 +75,9 @@ const MainWindow = new Lang.Class({
                             state: new GLib.Variant('s', 'punctuation') },
                           { name: 'character',
                             activate: this._character,
+                            parameter_type: new GLib.VariantType('s') },
+                          { name: 'filter-font',
+                            activate: this._filterFont,
                             parameter_type: new GLib.VariantType('s') }]);
 
         this.bind_property('search-active', this._search_active_button, 'active',
@@ -89,6 +92,9 @@ const MainWindow = new Lang.Class({
         this._search_entry.connect('search-changed',
                                    Lang.bind(this, this._handleSearchChanged));
 
+        this._menu_popover = new Menu.MenuPopover({});
+        this._menu_button.set_popover(this._menu_popover);
+
         this._categoryList = new CategoryList.CategoryListWidget();
         this._sidebar_grid.add(this._categoryList);
 
@@ -128,6 +134,8 @@ const MainWindow = new Lang.Class({
     },
 
     _handleKeyPress: function(self, event) {
+        if (this._menu_popover.visible)
+            return false;
         return this._search_bar.handle_event(event);
     },
 
@@ -153,6 +161,15 @@ const MainWindow = new Lang.Class({
         });
     },
 
+    _updateTitle: function(title, filterFont) {
+        if (filterFont) {
+            this._main_headerbar.title =
+                _("%s (%s only)").format(Gettext.gettext(title), filterFont);
+        } else {
+            this._main_headerbar.title = Gettext.gettext(title);
+        }
+    },
+
     _category: function(action, v) {
         let [name, length] = v.get_string()
 
@@ -167,13 +184,19 @@ const MainWindow = new Lang.Class({
 
         Util.assertNotEqual(category, null);
         this._mainView.setPage(category.name);
-        this._main_headerbar.title = Gettext.gettext(category.title);
+        this._updateTitle(category.title, null);
     },
 
     _character: function(action, v) {
         let [uc, length] = v.get_string()
         this._mainView.selectCharacter(uc);
     },
+
+    _filterFont: function(action, v) {
+        let [family, length] = v.get_string()
+        this._mainView.setFilterFont(family);
+        this._updateTitle(this._mainView.visible_child.title, family);
+    },
 });
 
 const MainView = new Lang.Class({
@@ -201,18 +224,22 @@ const MainView = new Lang.Class({
         params = Params.fill(params, { hexpand: true, vexpand: true });
         this.parent(params);
 
-        this._characterListWidgets = {};
+        this._characterLists = {};
 
         let characterList;
         for (let index in CategoryList.Category) {
             let category = CategoryList.Category[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);
         }
 
         characterList = this._createCharacterList(
             'search-result', _('Search Result Character List'));
+        // FIXME: Can't use GtkContainer.child_get_property.
+        characterList.title = "Search Result";
         this.add_named(characterList, 'search-result');
 
         // FIXME: Can't use GSettings.bind with 'as' from Gjs
@@ -225,13 +252,16 @@ const MainView = new Lang.Class({
     },
 
     _createCharacterList: function(name, accessible_name) {
-        let widget = new CharacterList.CharacterListWidget({ hexpand: true,
-                                                             vexpand: true });
+        let characterList = new CharacterList.CharacterListView({});
+        characterList.get_accessible().accessible_name = accessible_name;
+
+        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._characterListWidgets[name] = widget;
-        widget.get_accessible().accessible_name = accessible_name;
-        return new CharacterList.CharacterListView({ characterList: widget });
+
+        this._characterLists[name] = characterList;
+        return characterList;
     },
 
     searchByKeywords: function(keywords) {
@@ -244,14 +274,15 @@ const MainView = new Lang.Class({
     },
 
     setPage: function(name) {
-        if (!(name in this._characterListWidgets))
+        if (!(name in this._characterLists))
             return;
 
         this.visible_child_name = name;
+        this.visible_child.setFilterFont(null);
 
-        let characterList = this._characterListWidgets[name];
         if (name == 'recent') {
             this.visible_child.setCharacters(this._recentCharacters);
+            this.visible_child.updateCharacterList();
         } else {
             let category = null;
             for (let index in CategoryList.Category) {
@@ -291,5 +322,9 @@ const MainView = new Lang.Class({
             if (response_id == Gtk.ResponseType.CLOSE)
                 dialog.destroy();
         });
+    },
+
+    setFilterFont: function(family) {
+        this.visible_child.setFilterFont(family);
     }
 });


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