Blob Blame History Raw
// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
//
// 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
// 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 Lang = imports.lang;
const Params = imports.params;
const GnomeDesktop = imports.gi.GnomeDesktop;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const Gettext = imports.gettext;
const Gc = imports.gi.Gc;
const Util = imports.util;

const CategoryList = [
    {
        name: 'emojis',
        category: Gc.Category.EMOJI,
        title: N_('Emojis'),
        icon_name: 'characters-emoji-smileys',
        action_name: 'category'
    },
    {
        name: 'letters',
        category: Gc.Category.LETTER,
        title: N_('Letters & Symbols'),
        icon_name: 'characters-latin-symbolic',
        action_name: 'category'
    }
];

const LetterCategoryList = [
    {
        name: 'punctuation',
        category: Gc.Category.LETTER_PUNCTUATION,
        title: N_('Punctuation'),
        icon_name: 'characters-punctuation-symbolic',
        action_name: 'subcategory'
    },
    {
        name: 'arrow',
        category: Gc.Category.LETTER_ARROW,
        title: N_('Arrows'),
        icon_name: 'characters-arrow-symbolic',
        action_name: 'subcategory'
    },
    {
        name: 'bullet',
        category: Gc.Category.LETTER_BULLET,
        title: N_('Bullets'),
        icon_name: 'characters-bullet-symbolic',
        action_name: 'subcategory'
    },
    {
        name: 'picture',
        category: Gc.Category.LETTER_PICTURE,
        title: N_('Pictures'),
        icon_name: 'characters-picture-symbolic',
        action_name: 'subcategory'
    },
    {
        name: 'currency',
        category: Gc.Category.LETTER_CURRENCY,
        title: N_('Currencies'),
        icon_name: 'characters-currency-symbolic',
        action_name: 'subcategory'
    },
    {
        name: 'math',
        category: Gc.Category.LETTER_MATH,
        title: N_('Math'),
        icon_name: 'characters-math-symbolic',
        action_name: 'subcategory'
    },
    {
        name: 'letters',
        category: Gc.Category.LETTER_LATIN,
        title: N_('Letters'),
        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: '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'
    }
];

const CategoryListRowWidget = new Lang.Class({
    Name: 'CategoryListRowWidget',
    Extends: Gtk.ListBoxRow,

    _init: function(params, category) {
        params = Params.fill(params, {});
        this.parent(params);
        this.category = category;
        this.get_accessible().accessible_name =
            _('%s Category List Row').format(category.title);

        let hbox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL });
        this.add(hbox);

        let pixbuf = Util.loadIcon(category.icon_name, 24);
        let image = Gtk.Image.new_from_pixbuf(pixbuf);
        image.get_style_context().add_class('category-icon');
        hbox.pack_start(image, false, false, 2);

        let label = new Gtk.Label({ label: Gettext.gettext(category.title),
                                    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 pixbuf = Util.loadIcon(category.secondary_icon_name, 16);
            let image = Gtk.Image.new_from_pixbuf(pixbuf);
            image.get_style_context().add_class('category-icon');
            hbox.pack_end(image, false, false, 2);
        }
    }
});

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._categoryList = filtered.categoryList;
        this.populateCategoryList();

        for (let index in this._categoryList) {
            let category = this._categoryList[index];
            let rowWidget = new CategoryListRowWidget({}, category);
            rowWidget.get_style_context().add_class('category');
            this.add(rowWidget);
        }
    },

    vfunc_row_selected: function(row) {
        if (row != null && row.selectable) {
            let toplevel = row.get_toplevel();
            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);
            if (engines) {
                for (let j in engines) {
                    let engine = engines[j];
                    let language = engine.get_language();
                    if (language != null)
                        this._ibusLanguageList[engine.get_name()] = language;
                }
            }
        } catch (e) {
            log("Failed to list engines: " + e.message);
        }
        this._finishBuildScriptList(sources);
    },

    _ensureIBusLanguageList: function(sources) {
        if (this._ibusLanguageList != null)
            return;

        this._ibusLanguageList = {};

        // Don't assume IBus is always available.
        let ibus;
        try {
            ibus = imports.gi.IBus;
        } catch (e) {
            this._finishBuildScriptList(sources);
            return;
        }

        ibus.init();
        let bus = new ibus.Bus();
        if (bus.is_connected()) {
            bus.list_engines_async(-1,
                                   null,
                                   Lang.bind(this, function (bus, res) {
                                       this._finishListEngines(sources, bus, res);
                                   }));
        } else
            this._finishBuildScriptList(sources);
    },

    _finishBuildScriptList: function(sources) {
        let xkbInfo = new GnomeDesktop.XkbInfo();
        let languages = [];
        for (let i in sources) {
            let [type, id] = sources[i];
            switch (type) {
            case 'xkb':
                // FIXME: Remove this check once gnome-desktop gets the
                // support for that.
                if (xkbInfo.get_languages_for_layout) {
                    languages = languages.concat(
                        xkbInfo.get_languages_for_layout(id));
                }
                break;
            case 'ibus':
                if (id in this._ibusLanguageList)
                    languages.push(this._ibusLanguageList[id]);
                break;
            }
        }

        // Add current locale language to languages.
        languages.push(Gc.get_current_language());

        let allScripts = [];
        for (let i in languages) {
            let language = GnomeDesktop.normalize_locale(languages[i]);
            if (language == null)
                continue;
            let scripts = Gc.get_scripts_for_language(languages[i]);
            for (let j in scripts) {
                let script = scripts[j];
                // Exclude Latin and Han, since Latin is always added
                // at the top and Han contains too many characters.
                if (['Latin', 'Han'].indexOf(script) >= 0)
                    continue;
                if (allScripts.indexOf(script) >= 0)
                    continue;
                allScripts.push(script);
            }
        }

        allScripts.unshift('Latin');
        let category = this.getCategory('letters');
        category.scripts = allScripts;
    },

    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/');
        if (settings) {
            let sources = settings.get_value('sources').deep_unpack();
            let hasIBus = sources.some(function(current, index, array) {
                return current[0] == 'ibus';
            });
            if (hasIBus)
                this._ensureIBusLanguageList(sources);
            else
                this._finishBuildScriptList(sources);
        }
    }
});

const EmojiCategoryListWidget = new Lang.Class({
    Name: 'EmojiCategoryListWidget',
    Extends: CategoryListWidget,

    _init: function(params) {
        params = Params.fill(params, {});
        this.parent(params);

        let category;
        let rowWidget;

        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) {
        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;
        }
    },

    getCategoryList: function() {
        return this._categoryList;
    }
});