Mini Shell

Direktori : /proc/self/root/usr/share/org.gnome.Characters/
Upload File :
Current File : //proc/self/root/usr/share/org.gnome.Characters/org.gnome.Characters.src.gresource

GVariant�(
	
Ե�����L��KFv��L��{W�y�v����$0�L��KP��L��c��3�v��2�����2L33��.G3v 3��g��	v���+Ka���vȣ`��(�
`�vh�������v�����	v��M/js/	
main.js�// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
//
// Copyright (c) 2013 Giovanni Campagna <scampa.giovanni@gmail.com>
//
// Redistribution and use in source and binary forms, with or without
//  modification, are permitted provided that the following conditions are met:
//   * Redistributions of source code must retain the above copyright
//     notice, this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright
//     notice, this list of conditions and the following disclaimer in the
//     documentation and/or other materials provided with the distribution.
//   * Neither the name of the GNOME Foundation nor the
//     names of its contributors may be used to endorse or promote products
//     derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

pkg.initGettext();
pkg.initFormat();
pkg.require({ 'Gdk': '3.0',
              'Gio': '2.0',
              'GLib': '2.0',
              'GObject': '2.0',
              'Gtk': '3.0' });

const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;

const Util = imports.util;
const Window = imports.window;

var settings = null;

function initEnvironment() {
    window.getApp = function() {
        return Gio.Application.get_default();
    };
}

const MyApplication = new Lang.Class({
    Name: 'MyApplication',
    Extends: Gtk.Application,

    _init: function() {
        this.parent({ application_id: pkg.name });

        GLib.set_application_name(_("Characters Application"));
    },

    _onQuit: function() {
        this.quit();
    },

    _onSearch: function(action, parameter) {
        let window = new Window.MainWindow({ application: this });
        window.setSearchKeywords(parameter.get_strv());
        window.show();
    },

    _initAppMenu: function() {
        let builder = new Gtk.Builder();
        builder.add_from_resource('/org/gnome/Characters/app-menu.ui');

        let menu = builder.get_object('app-menu');
        this.set_app_menu(menu);
    },

    vfunc_startup: function() {
        this.parent();

        Util.loadStyleSheet('/org/gnome/Characters/application.css');

        Util.initActions(this,
                         [{ name: 'quit',
                            activate: this._onQuit },
                          { name: 'search',
                            activate: this._onSearch,
                            parameter_type: new GLib.VariantType('as') }]);
        this._initAppMenu();

        settings = Util.getSettings('org.gnome.Characters',
                                    '/org/gnome/Characters/');

        log(_("Characters Application started"));
    },

    vfunc_activate: function() {
        (new Window.MainWindow({ application: this })).show();
    },

    vfunc_shutdown: function() {
        log(_("Characters Application exiting"));

        this.parent();
    }
});

function main(argv) {
    initEnvironment();

    return (new MyApplication()).run(argv);
}
(uuay)gnome/org/character.js�!// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
//
// Copyright (C) 2014-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 Lang = imports.lang;
const Params = imports.params;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Pango = imports.gi.Pango;
const Gc = imports.gi.Gc;
const Main = imports.main;
const Util = imports.util;

var CharacterDialog = new Lang.Class({
    Name: 'CharacterDialog',
    Extends: Gtk.Dialog,
    Signals: {
        'character-copied': { param_types: [ GObject.TYPE_STRING ] }
    },
    Template: 'resource:///org/gnome/Characters/character.ui',
    InternalChildren: ['main-stack', 'character-stack',
                       'character-label', 'missing-label', 'detail-label',
                       'copy-button', 'copy-revealer', 'related-listbox'],

    _init: function(params) {
        let filtered = Params.filter(params, { character: null,
                                               fontDescription: null });
        params = Params.fill(params, { use_header_bar: true,
                                       width_request: 400,
                                       height_request: 400 });
        this.parent(params);

        this._cancellable = new Gio.Cancellable();

        this._copy_button.connect('clicked', Lang.bind(this, this._copyCharacter));

        this._related_listbox.connect('row-selected',
                                      Lang.bind(this, this._handleRowSelected));

        this._relatedButton = new Gtk.ToggleButton({ label: _("See Also") });
        this.add_action_widget(this._relatedButton, Gtk.ResponseType.HELP);
        this._relatedButton.show();

        this._relatedButton.connect(
            'toggled',
            Lang.bind(this, function() {
                if (this._main_stack.visible_child_name == 'character')
                    this._main_stack.visible_child_name = 'related';
                else
                    this._main_stack.visible_child_name = 'character';
            }));

        this._fontDescription = filtered.fontDescription;
        this._setCharacter(filtered.character);

        this._copyRevealerTimeoutId = 0;
    },

    _finishSearch: function(result) {
        let children = this._related_listbox.get_children();
        for (let index in children)
            this._related_listbox.remove(children[index]);

        for (let index = 0; index < result.len; index++) {
            let uc = Gc.search_result_get(result, index);
            let name = Gc.character_name(uc);
            if (name == null)
                continue;

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

            let characterLabel = new Gtk.Label({ label: uc,
                                                 valign: Gtk.Align.CENTER,
                                                 halign: Gtk.Align.CENTER,
                                                 width_request: 45 });
            characterLabel.get_style_context().add_class('character');
            hbox.pack_start(characterLabel, false, false, 2);

            let nameLabel = new Gtk.Label({ label: Util.capitalize(name),
                                            halign: Gtk.Align.START,
                                            ellipsize: Pango.EllipsizeMode.END });
            hbox.pack_start(nameLabel, true, true, 0);

            let row = new Gtk.ListBoxRow();
            row._character = uc;
            row.add(hbox);
            row.show_all();

            this._related_listbox.add(row);
        }

        this._relatedButton.visible =
            this._related_listbox.get_children().length > 0;
    },

    _setCharacter: function(uc) {
        this._character = uc;

        let codePoint = Util.toCodePoint(this._character);
        let codePointHex = codePoint.toString(16).toUpperCase();

        let name = Gc.character_name(this._character);
        if (name != null) {
            name = Util.capitalize(name);
        } else {
            name = _("Unicode U+%04s").format(codePointHex);
        }

        let headerBar = this.get_header_bar();
        headerBar.title = name;

        this._character_label.override_font(this._fontDescription);
        this._character_label.label = this._character;

        var pangoContext = this._character_label.get_pango_context();
        var pangoLayout = Pango.Layout.new(pangoContext);
        pangoLayout.set_text(this._character, -1);
        if (pangoLayout.get_unknown_glyphs_count() == 0) {
            this._character_stack.visible_child_name = 'character';
        } else {
            var fontFamily = this._fontDescription.get_family();
            this._missing_label.label =
                // TRANSLATORS: the first variable is a character, the second is a font
                _("%s is not included in %s").format(name, fontFamily);
            this._character_stack.visible_child_name = 'missing';
        }

        this._detail_label.label = _("Unicode U+%04s").format(codePointHex);

        this._cancellable.cancel();
        this._cancellable.reset();
        let criteria = Gc.SearchCriteria.new_related(this._character);
        let context = new Gc.SearchContext({ criteria: criteria });
        context.search(
            -1,
            this._cancellable,
            Lang.bind(this, function(context, res, user_data) {
                try {
                    let result = context.search_finish(res);
                    this._finishSearch(result);
                } catch (e) {
                    log("Failed to search related: " + e.message);
                }
            }));

        this._relatedButton.active = false;
        this._main_stack.visible_child_name = 'character';
        this._main_stack.show_all();
    },

    _hideCopyRevealer: function() {
        if (this._copyRevealerTimeoutId > 0) {
            GLib.source_remove(this._copyRevealerTimeoutId);
            this._copyRevealerTimeoutId = 0;
            this._copy_revealer.set_reveal_child(false);
        }
    },

    _clipboardOwnerChanged: function(clipboard, event) {
        let text = clipboard.wait_for_text();
        if (text != this._character)
            this._hideCopyRevealer();
    },

    _copyCharacter: function() {
        if (this._clipboard == null) {
            this._clipboard = Gc.gtk_clipboard_get();
            let clipboardOwnerChanged =
                this._clipboard.connect('owner-change',
                                        Lang.bind(this,
                                                  this._clipboardOwnerChanged));
            this.connect('destroy',
                         Lang.bind(this, function() {
                             this._clipboard.disconnect(clipboardOwnerChanged);
                         }));
        }
        this._clipboard.set_text(this._character, -1);
        this.emit('character-copied', this._character);

        // Show a feedback message with a revealer.  The message is
        // hidden after 2 seconds, or when another client set a
        // different text to clipboard.
        this._hideCopyRevealer();
        this._copy_revealer.set_reveal_child(true);
        this._copyRevealerTimeoutId =
            GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2000,
                             Lang.bind(this, this._hideCopyRevealer));
        this.connect('destroy',
                     Lang.bind(this, function() {
                         if (this._copyRevealerTimeoutId > 0)
                             GLib.source_remove(this._copyRevealerTimeoutId);
                     }));
    },

    _handleRowSelected: function(listBox, row) {
        if (row != null) {
            this._setCharacter(row._character);
            let toplevel = this.get_transient_for();
            let action = toplevel.lookup_action('character');
            action.activate(new GLib.Variant('s', row._character));
        }
    },
});
(uuay)Characters/characterList.js�b// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
//
// Copyright (C) 2014-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 Lang = imports.lang;
const Params = imports.params;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Gdk = imports.gi.Gdk;
const Cairo = imports.cairo;
const Pango = imports.gi.Pango;
const PangoCairo = imports.gi.PangoCairo;
const Gc = imports.gi.Gc;
const Main = imports.main;
const Util = imports.util;

const BASELINE_OFFSET = 0.85;
const CELLS_PER_ROW = 5;
const NUM_ROWS = 5;
const NUM_COLUMNS = 5;
const CELL_SIZE = 50;

function getCellSize(fontDescription) {
    if (fontDescription == null
        || fontDescription.get_size() == 0)
        return CELL_SIZE;
    return fontDescription.get_size() * 2 / Pango.SCALE;
}

const CharacterListRow = new Lang.Class({
    Name: 'CharacterListRow',
    Extends: GObject.Object,

    _init: function(params) {
        let filtered = Params.filter(params, { characters: null,
                                               fontDescription: null,
                                               overlayFontDescription: null });
        params = Params.fill(params, {});
        this.parent(params);
        this._characters = filtered.characters;
        this._fontDescription = filtered.fontDescription;
        this._overlayFontDescription = filtered.overlayFontDescription;
    },

    draw: function(cr, x, y, width, height) {
        let layout = PangoCairo.create_layout(cr);
        layout.set_font_description(this._fontDescription);

        // Draw baseline.
        // FIXME: Pick the baseline color from CSS.
        cr.setSourceRGBA(114.0 / 255.0, 159.0 / 255.0, 207.0 / 255.0, 1.0);
        cr.setLineWidth(0.5);
        cr.moveTo(x, y + BASELINE_OFFSET * height);
        cr.relLineTo(width, 0);
        cr.stroke();
        cr.setSourceRGBA(0.0, 0.0, 0.0, 1.0);

        // Draw characters.  Do centering and attach to the baseline.
        let cellSize = getCellSize(this._fontDescription);
        for (let i in this._characters) {
            var cellRect = new Gdk.Rectangle({ x: x + cellSize * i,
                                               y: y,
                                               width: cellSize,
                                               height: cellSize });
            if (Gc.character_is_invisible(this._characters[i])) {
                this._drawBoundingBox(cr, cellRect, this._characters[i]);
                this._drawCharacterName(cr, cellRect, this._characters[i]);
            } else {
                layout.set_text(this._characters[i], -1);
                if (layout.get_unknown_glyphs_count () == 0) {
                    let layoutBaseline = layout.get_baseline();
                    let [logicalRect, inkRect] = layout.get_extents();
                    cr.moveTo(x + cellSize * i - logicalRect.x / Pango.SCALE +
                              (cellSize - logicalRect.width / Pango.SCALE) / 2,
                              y + BASELINE_OFFSET * height -
                              layoutBaseline / Pango.SCALE);
                    PangoCairo.show_layout(cr, layout);
                } else {
                    this._drawBoundingBox(cr, cellRect, this._characters[i]);
                    this._drawCharacterName(cr, cellRect, this._characters[i]);
                }
            }
        }
    },

    _computeBoundingBox: function(cr, cellRect, uc) {
        let layout = PangoCairo.create_layout(cr);
        layout.set_font_description(this._fontDescription);
        layout.set_text(uc, -1);

        let shapeRect;
        let layoutBaseline;
        if (layout.get_unknown_glyphs_count() == 0) {
            let [logicalRect, inkRect] = layout.get_extents();
            layoutBaseline = layout.get_baseline();
            shapeRect = inkRect;
        } else {
            // If the character cannot be rendered with the current
            // font settings, show a rectangle calculated from the
            // base glyph ('A').
            if (this._baseGlyphRect == null) {
                layout.set_text('A', -1);
                let [baseLogicalRect, baseInkRect] = layout.get_extents();
                this._baseGlyphLayoutBaseline = layout.get_baseline();
                this._baseGlyphRect = baseInkRect;
            }
            layoutBaseline = this._baseGlyphLayoutBaseline;
            shapeRect = new Pango.Rectangle({
                x: this._baseGlyphRect.x,
                y: this._baseGlyphRect.y,
                width: this._baseGlyphRect.width,
                height: this._baseGlyphRect.height
            });
            let characterWidth = Gc.character_width (uc);
            if (characterWidth > 1)
                shapeRect.width *= characterWidth;
        }

        shapeRect.x = cellRect.x - shapeRect.x / Pango.SCALE +
            (cellRect.width - shapeRect.width / Pango.SCALE) / 2;
        shapeRect.y = cellRect.y + BASELINE_OFFSET * cellRect.height -
            layoutBaseline / Pango.SCALE;
        shapeRect.width = shapeRect.width / Pango.SCALE;
        shapeRect.height = shapeRect.height / Pango.SCALE;
        return shapeRect;
    },

    _drawBoundingBox: function(cr, cellRect, uc) {
        cr.save();
        cr.rectangle(cellRect.x, cellRect.y, cellRect.width, cellRect.height);
        cr.clip();

        let layout = PangoCairo.create_layout(cr);
        layout.set_font_description(this._fontDescription);
        layout.set_text(uc, -1);
        let shapeRect = this._computeBoundingBox(cr, cellRect, uc);

        let borderWidth = 1;
        cr.rectangle(shapeRect.x - borderWidth * 2,
                     shapeRect.y - borderWidth * 2,
                     shapeRect.width + borderWidth * 2,
                     shapeRect.height + borderWidth * 2);
        cr.setSourceRGBA(239.0 / 255.0, 239.0 / 255.0, 239.0 / 255.0, 1.0);
        cr.fill();

        cr.restore();
    },

    _drawCharacterName: function(cr, cellRect, uc) {
        cr.save();
        cr.rectangle(cellRect.x, cellRect.y, cellRect.width, cellRect.height);
        cr.clip();

        let layout = PangoCairo.create_layout(cr);
        layout.set_width(cellRect.width * Pango.SCALE * 0.8);
        layout.set_height(cellRect.height * Pango.SCALE * 0.8);
        layout.set_wrap(Pango.WrapMode.WORD);
        layout.set_ellipsize(Pango.EllipsizeMode.END);
        layout.set_alignment(Pango.Alignment.CENTER);
        layout.set_font_description(this._overlayFontDescription);
        let name = Gc.character_name(uc);
        let text = name == null ? _('Unassigned') : Util.capitalize(name);
        layout.set_text(text, -1);
        let [logicalRect, inkRect] = layout.get_extents();
        cr.moveTo(cellRect.x - logicalRect.x / Pango.SCALE +
                  (cellRect.width - logicalRect.width / Pango.SCALE) / 2,
                  cellRect.y - logicalRect.y / Pango.SCALE +
                  (cellRect.height - logicalRect.height / Pango.SCALE) / 2);
        cr.setSourceRGBA(0.0, 0.0, 0.0, 1.0);
        PangoCairo.show_layout(cr, layout);

        cr.restore();
    }
});

const CharacterListWidget = new Lang.Class({
    Name: 'CharacterListWidget',
    Extends: Gtk.DrawingArea,
    Signals: {
        'character-selected': { param_types: [ GObject.TYPE_STRING ] }
    },

    _init: function(params) {
        let filtered = Params.filter(params, {
            fontDescription: null,
            numRows: NUM_ROWS
        });
        params = Params.fill(params, {});
        this.parent(params);
        let context = this.get_style_context();
        context.add_class('character-list');
        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 |
                        Gdk.EventMask.BUTTON_RELEASE_MASK);
        this._character = null;
        this.drag_source_set(Gdk.ModifierType.BUTTON1_MASK,
                             null,
                             Gdk.DragAction.COPY);
        this.drag_source_add_text_targets();
    },

    vfunc_drag_begin: function(context) {
        let cellSize = getCellSize(this._fontDescription);
        this._dragSurface = new Cairo.ImageSurface(Cairo.Format.ARGB32,
                                                   cellSize,
                                                   cellSize);
        let cr = new Cairo.Context(this._dragSurface);
        cr.setSourceRGBA(1.0, 1.0, 1.0, 1.0);
        cr.paint();
        cr.setSourceRGBA(0.0, 0.0, 0.0, 1.0);
        let row = this._createCharacterListRow([this._character]);
        row.draw(cr, 0, 0, cellSize, cellSize);
        Gtk.drag_set_icon_surface(context, this._dragSurface, 0, 0);
    },

    vfunc_drag_data_get: function(context, data, info, time) {
        if (this._character != null)
            data.set_text(this._character, -1);
    },

    vfunc_button_press_event: function(event) {
        let allocation = this.get_allocation();
        let cellSize = getCellSize(this._fontDescription);
        let x = Math.floor(event.x / cellSize);
        let y = Math.floor(event.y / cellSize);
        let index = y * this._cellsPerRow + x;
        if (index < this._characters.length)
            this._character = this._characters[index];
        else
            this._character = null;
        return false;
    },

    vfunc_button_release_event: function(event) {
        if (this._character)
            this.emit('character-selected', this._character);
        return false;
    },

    vfunc_get_request_mode: function() {
        return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
    },

    vfunc_get_preferred_height: function() {
        let [minWidth, natWidth] = this.vfunc_get_preferred_width();
        return this.vfunc_get_preferred_height_for_width(minWidth);
    },

    vfunc_get_preferred_height_for_width: function(width) {
        let height = Math.max(this._rows.length, this._numRows) *
            getCellSize(this._fontDescription);
        return [height, height];
    },

    vfunc_get_preferred_width: function() {
        return this.vfunc_get_preferred_width_for_height(0);
    },

    vfunc_get_preferred_width_for_height: function(height) {
        let cellSize = getCellSize(this._fontDescription);
        let minWidth = NUM_COLUMNS * cellSize;
        let natWidth = Math.max(this._cellsPerRow, NUM_COLUMNS) * cellSize;
        return [minWidth, natWidth];
    },

    vfunc_size_allocate: function(allocation) {
        this.parent(allocation);

        let cellSize = getCellSize(this._fontDescription);
        let cellsPerRow = Math.floor(allocation.width / cellSize);
        if (cellsPerRow != this._cellsPerRow) {
            // Reflow if the number of cells per row has changed.
            this._cellsPerRow = cellsPerRow;
            this.setCharacters(this._characters);
        }
    },

    _createCharacterListRow: function(characters) {
        var context = this.get_pango_context();
        var fontDescription = context.get_font_description();
        fontDescription.set_size(fontDescription.get_size() * 0.8);
        let row = new CharacterListRow({
            characters: characters,
            fontDescription: this._fontDescription,
            overlayFontDescription: fontDescription
        });
        return row;
    },

    setFontDescription: function(fontDescription) {
        this._fontDescription = fontDescription;
    },

    setCharacters: function(characters) {
        this._rows = [];
        this._characters = characters;

        let start = 0, stop = 1;
        for (; stop <= characters.length; stop++) {
            if (stop % this._cellsPerRow == 0) {
                let rowCharacters = characters.slice(start, stop);
                let row = this._createCharacterListRow(rowCharacters);
                this._rows.push(row);
                start = stop;
            }
        }
        if (start != stop - 1) {
            let rowCharacters = characters.slice(start, stop);
            let row = this._createCharacterListRow(rowCharacters);
            this._rows.push(row);
        }

        this.queue_resize();
        this.queue_draw();
    },

    vfunc_draw: function(cr) {
        // Clear the canvas.
        let context = this.get_style_context();
        let fg = context.get_color(Gtk.StateFlags.NORMAL);
        let bg = context.get_background_color(Gtk.StateFlags.NORMAL);

        cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
        cr.paint();
        cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);

        // Use device coordinates directly, since PangoCairo doesn't
        // work well with scaled matrix:
        // https://bugzilla.gnome.org/show_bug.cgi?id=700592
        let allocation = this.get_allocation();

        // Redraw rows within the clipped region.
        let [x1, y1, x2, y2] = cr.clipExtents();
        let cellSize = getCellSize(this._fontDescription);
        let start = Math.max(0, Math.floor(y1 / cellSize));
        let end = Math.min(this._rows.length, Math.ceil(y2 / cellSize));
        for (let index = start; index < end; index++) {
            this._rows[index].draw(cr, 0, index * cellSize,
                                   allocation.width, cellSize);
        }
    }
});

const MAX_SEARCH_RESULTS = 100;

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

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

    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);

        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
        });
        scroll.add(this._characterList);
        let context = scroll.get_style_context();
        context.add_class('character-list-scroll');
        context.save();
        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;
        this._cancellable = new Gio.Cancellable();
        this._cancellable.connect(Lang.bind(this, function () {
            this._stopSpinner();
            this._searchContext = null;
            this._characters = [];
            this._updateCharacterList();
        }));
        scroll.connect('edge-reached', Lang.bind(this, this._onEdgeReached));
        scroll.connect('size-allocate', Lang.bind(this, this._onSizeAllocate));
    },

    _startSpinner: function() {
        this._stopSpinner();
        this._spinnerTimeoutId =
            GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000,
                             Lang.bind(this, function () {
                                 this._loading_spinner.start();
                                 this.visible_child_name = 'loading';
                                 this.show_all();
                             }));
    },

    _stopSpinner: function() {
        if (this._spinnerTimeoutId > 0) {
            GLib.source_remove(this._spinnerTimeoutId);
            this._spinnerTimeoutId = 0;
            this._loading_spinner.stop();
        }
    },

    _finishSearch: function(result) {
        this._stopSpinner();

        let characters = Util.searchResultToArray(result);

        this.setCharacters(characters);
    },

    setCharacters: function(characters) {
        this._characters = characters;
        this._updateCharacterList();
    },

    _updateCharacterList: function() {
        let [fontDescription, characters] = this._fontFilter.apply(this, this._characters);
        this._characterList.setFontDescription(fontDescription);
        this._characterList.setCharacters(characters);
        if (characters.length == 0) {
            this.visible_child_name = 'unavailable';
        } else {
            this.visible_child_name = 'character-list';
        }
        this.show_all();
    },

    _maybeLoadMore() {
        if (this._searchContext != null && !this._searchContext.is_finished()) {
            this._searchWithContext(this._searchContext, MAX_SEARCH_RESULTS);
        }
    },

    _onEdgeReached: function(scrolled, pos) {
        if (pos == Gtk.PositionType.BOTTOM) {
            this._maybeLoadMore();
        }
    },

    get initialSearchCount() {
        // Use our parents allocation; we aren't visible before we do the
        // initial search, so our allocation is 1x1
        let allocation = this.get_parent().get_allocation();

        // 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._fontFilter.fontDescription);
        let cellsPerRow = Math.floor(allocation.width / cellSize);
        // Ensure the rows cause a scroll
        let heightInRows = Math.ceil((allocation.height + 1) / cellSize);

        return Math.max(MAX_SEARCH_RESULTS, heightInRows * cellsPerRow);
    },

    _onSizeAllocate: function(scrolled, allocation) {
        if (this._characters.length < this.initialSearchCount) {
            this._maybeLoadMore();
        }
    },

    _addSearchResult: function(result) {
        let characters = Util.searchResultToArray(result);
        this.setCharacters(this._characters.concat(characters));
    },

    _searchWithContext: function(context, count) {
        this._startSpinner();
        context.search(
            count,
            this._cancellable,
            Lang.bind(this, function(context, res, user_data) {
                this._stopSpinner();
                try {
                    let result = context.search_finish(res);
                    this._addSearchResult(result);
                } catch (e) {
                    log("Failed to search: " + e.message);
                }
            }));
    },

    searchByCategory: function(category) {
        if ('scripts' in category) {
            this.searchByScripts(category.scripts);
            return;
        }

        let criteria = Gc.SearchCriteria.new_category(category.category);
        this._searchContext = new Gc.SearchContext({ criteria: criteria });
        this._searchWithContext(this._searchContext, this.initialSearchCount);
    },

    searchByKeywords: function(keywords) {
        let criteria = Gc.SearchCriteria.new_keywords(keywords);
        this._searchContext = new Gc.SearchContext({
            criteria: criteria,
            flags: Gc.SearchFlag.WORD
        });
        this._searchWithContext(this._searchContext, this.initialSearchCount);
    },

    searchByScripts: function(scripts) {
        var criteria = Gc.SearchCriteria.new_scripts(scripts);
        this._searchContext = new Gc.SearchContext({ criteria: criteria });
        this._searchWithContext(this._searchContext, this.initialSearchCount);
    },

    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 ] }
    },

    _init: function(params) {
        let filtered = Params.filter(params, {
            category: null,
            fontFilter: null
        });
        params = Params.fill(params, {
            hexpand: true, vexpand: false
        });
        this.parent(params);

        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();
    }
});
(uuay)params.js�
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

// Params:
//
// A set of convenience functions for dealing with pseudo-keyword
// arguments.
//
// Examples:
//
// A function with complex arguments
// function myFunction(params) {
//     params = Params.parse(params, { myFlags: Flags.NONE,
//                                     anInt: 42,
//                                     aString: 'hello, world!',
//                                    });
//     ... params.anInt, params.myFlags, params.aString ...
// }
// myFunction({ anInt: -1 });
//
// Extend a method to allow more params in a subclass
// The superclass can safely use Params.parse(), it won't see
// the extensions.
// const MyClass = new Lang.Class({
//       ...
//       method: function(params) {
//           let mine = Params.filter(params, { anInt: 42 });
//           this.parent(params);
//           ... mine.anInt ...
//       }
// });

// parse:
// @params: caller-provided parameter object, or %null
// @defaults: function-provided defaults object
//
// Examines @params and fills in default values from @defaults for
// any properties in @defaults that don't appear in @params.
// This function will throw a Error if @params contains a property
// that is not recognized. Use fill() or filter() if you don't
// want that.
//
// If @params is %null, this returns the values from @defaults.
//
// Return value: a new object, containing the merged parameters from
// @params and @defaults
function parse(params, defaults) {
    let ret = {}, prop;
    params = params || {};

    for (prop in params) {
        if (!(prop in defaults))
            throw new Error('Unrecognized parameter "' + prop + '"');
        ret[prop] = params[prop];
    }

    for (prop in defaults) {
        if (!(prop in params))
            ret[prop] = defaults[prop];
    }

    return ret;
}

// fill:
// @params: caller-provided parameter object, or %null
// @defaults: function-provided defaults object
//
// Examines @params and fills in default values from @defaults
// for any properties in @defaults that don't appear in @params.
//
// Differently from parse(), this function does not throw for
// unrecognized parameters.
//
// Return value: a new object, containing the merged parameters from
// @params and @defaults
function fill(params, defaults) {
    let ret = {}, prop;
    params = params || {};

    for (prop in params)
        ret[prop] = params[prop];

    for (prop in defaults) {
        if (!(prop in ret))
            ret[prop] = defaults[prop];
    }

    return ret;
}

// filter:
// @params: caller-provided parameter object, or %null
// @defaults: function-provided defaults object
//
// Examines @params and returns an object containing the
// same properties as @defaults, but with values taken from
// @params where available.
// Then it removes from @params all matched properties.
//
// This is similar to parse(), but it accepts unknown properties
// and modifies @params for known ones.
//
// If @params is %null, this returns the values from @defaults.
//
// Return value: a new object, containing the merged parameters from
// @params and @defaults
function filter(params, defaults) {
    let ret = {}, prop;
    params = params || {};

    for (prop in defaults) {
        if (!(prop in params))
            ret[prop] = defaults[prop];
    }

    for (prop in params) {
        if (prop in defaults) {
            ret[prop] = params[prop];
            delete params[prop];
        }
    }

    return ret;
}
(uuay)menu.js�// -*- 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;

var MenuPopover = new Lang.Class({
    Name: 'MenuPopover',
    Extends: Gtk.Popover,
    Template: 'resource:///org/gnome/Characters/menu.ui',
    InternalChildren: ['search-entry', 'font-listbox'],

    _createFontListRow: function(title, family) {
        let row = new Gtk.ListBoxRow({ visible: true });
        row.get_style_context().add_class('font');
        row._family = family;
        let label = new Gtk.Label({ label: title,
                                    visible: true,
                                    halign: Gtk.Align.START });
        label.get_style_context().add_class('font-label');
        row.add(label);
        return row;
    },

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

        this._font_listbox.get_style_context().add_class('fonts');
        let row = this._createFontListRow(_("None"), 'None');
        this._font_listbox.add(row);

        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) {
            row = this._createFontListRow(families[index].get_name(),
                                          families[index].get_name());
            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._font_listbox.set_header_func(Lang.bind(this, this._headerFunc));

        // This silents warning at Characters exit about this widget being
        // visible but not mapped.  Borrowed from Maps.
        this.connect('unmap', function(popover) {
            popover._font_listbox.unselect_all();
            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));
        }
    },

    _filterFunc: function(row) {
        if (this._keywords.length == 0)
            return true;
        if (row._family == 'None')
            return true;

        let nameWords = row._family.split(/\s+/).map(String.toLowerCase);
        return this._keywords.every(function(keyword, index, array) {
            return nameWords.some(function(nameWord, index, array) {
                return nameWord.indexOf(keyword) >= 0;
            });
        });
    },

    _headerFunc: function(row, before) {
        if (before && !row.get_header()) {
            let separator = new Gtk.Separator({
                orientation: Gtk.Orientation.HORIZONTAL
            });
            row.set_header (separator);
        }
    }
});
(uuay)util.js�// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
//
// Copyright (c) 2013 Giovanni Campagna <scampa.giovanni@gmail.com>
//
// Redistribution and use in source and binary forms, with or without
//  modification, are permitted provided that the following conditions are met:
//   * Redistributions of source code must retain the above copyright
//     notice, this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright
//     notice, this list of conditions and the following disclaimer in the
//     documentation and/or other materials provided with the distribution.
//   * Neither the name of the GNOME Foundation nor the
//     names of its contributors may be used to endorse or promote products
//     derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (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 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;

function loadUI(resourcePath, objects) {
    let ui = new Gtk.Builder();

    if (objects) {
        for (let o in objects)
            ui.expose_object(o, objects[o]);
    }

    ui.add_from_resource(resourcePath);
    return ui;
}

function loadStyleSheet(resource) {
    let provider = new Gtk.CssProvider();
    provider.load_from_file(Gio.File.new_for_uri('resource://' + resource));
    Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
                                             provider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
}

function initActions(actionMap, simpleActionEntries, context) {
    simpleActionEntries.forEach(function(entry) {
        let filtered = Params.filter(entry, { activate: null,
                                              state_changed: null,
                                              context: null });
        let action = new Gio.SimpleAction(entry);

        let context = filtered.context || actionMap;
        if (filtered.activate)
            action.connect('activate', filtered.activate.bind(context));
        if (filtered.state_changed)
            action.connect('state-changed', filtered.state_changed.bind(context));

        actionMap.add_action(action);
    });
}

function arrayEqual(one, two) {
    if (one.length != two.length)
        return false;

    for (let i = 0; i < one.length; i++)
        if (one[i] != two[i])
            return false;

    return true;
}

function getSettings(schemaId, path) {
    const GioSSS = Gio.SettingsSchemaSource;
    let schemaSource;

    if (!pkg.moduledir.startsWith('resource://')) {
        // Running from the source tree
        schemaSource = GioSSS.new_from_directory(pkg.pkgdatadir,
                                                 GioSSS.get_default(),
                                                 false);
    } else {
        schemaSource = GioSSS.get_default();
    }

    let schemaObj = schemaSource.lookup(schemaId, true);
    if (!schemaObj) {
        log('Missing GSettings schema ' + schemaId);
        System.exit(1);
    }

    if (path === undefined)
        return new Gio.Settings({ settings_schema: schemaObj });
    else
        return new Gio.Settings({ settings_schema: schemaObj,
                                  path: path });
}

function loadIcon(iconName, size) {
    let theme = Gtk.IconTheme.get_default();

    return theme.load_icon(iconName,
                           size,
                           Gtk.IconLookupFlags.GENERIC_FALLBACK);
}

function assertEqual(one, two) {
    if (one != two)
        throw Error('Assertion failed: ' + one + ' != ' + two);
}

function assertNotEqual(one, two) {
    if (one == two)
        throw Error('Assertion failed: ' + one + ' == ' + two);
}

function capitalizeWord(w) {
    if (w.length > 0)
        return w[0].toUpperCase() + w.slice(1).toLowerCase()
    return w;
}

function capitalize(s) {
    return s.split(/\s+/).map(function(w) {
        let acronyms = ["CJK"];
        if (acronyms.indexOf(w) > -1)
            return w;
        let prefixes = ["IDEOGRAPH-", "SELECTOR-"];
        for (let index in prefixes) {
            let prefix = prefixes[index];
            if (w.startsWith(prefix))
                return capitalizeWord(prefix) + w.slice(prefix.length);
        }
        return capitalizeWord(w);
    }).join(' ');
}

function toCodePoint(s) {
    let codePoint = s.charCodeAt(0);
    if (codePoint >= 0xD800 && codePoint <= 0xDBFF) {
        let high = codePoint;
        let low = s.charCodeAt(1);
        codePoint = 0x10000 + (high - 0xD800) * 0x400 + (low - 0xDC00);
    }

    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;
}
(uuay)categoryList.jsj8// -*- 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;
    }
});
(uuay)window.js<I// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
//
// Copyright (c) 2013 Giovanni Campagna <scampa.giovanni@gmail.com>
//
// Redistribution and use in source and binary forms, with or without
//  modification, are permitted provided that the following conditions are met:
//   * Redistributions of source code must retain the above copyright
//     notice, this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright
//     notice, this list of conditions and the following disclaimer in the
//     documentation and/or other materials provided with the distribution.
//   * Neither the name of the GNOME Foundation nor the
//     names of its contributors may be used to endorse or promote products
//     derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (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;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Params = imports.params;
const CategoryList = imports.categoryList;
const Character = imports.character;
const CharacterList = imports.characterList;
const Menu = imports.menu;
const Gettext = imports.gettext;

const Main = imports.main;
const Util = imports.util;

var MainWindow = new Lang.Class({
    Name: 'MainWindow',
    Extends: Gtk.ApplicationWindow,
    Template: 'resource:///org/gnome/Characters/mainwindow.ui',
    InternalChildren: ['main-headerbar', 'search-active-button',
                       'search-bar', 'search-entry', 'back-button',
                       'menu-button',
                       'main-grid', 'main-hbox', 'sidebar-grid'],
    Properties: {
        'search-active': GObject.ParamSpec.boolean(
            'search-active', '', '',
            GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE, false)
    },

    _init: function(params) {
        params = Params.fill(params, { title: GLib.get_application_name(),
                                       default_width: 640,
                                       default_height: 480 });
        this.parent(params);

        this._searchActive = false;
        this._searchKeywords = [];

        Util.initActions(this,
                         [{ name: 'about',
                            activate: this._about },
                          { name: 'search-active',
                            activate: this._toggleSearch,
                            parameter_type: new GLib.VariantType('b'),
                            state: new GLib.Variant('b', false) },
                          { name: 'find',
                            activate: this._find },
                          { name: 'category',
                            activate: this._category,
                            parameter_type: new GLib.VariantType('s'),
                            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') },
                          { name: 'filter-font',
                            activate: this._filterFont,
                            parameter_type: new GLib.VariantType('s') }]);

        this.application.set_accels_for_action('win.find', ['<Primary>F']);

        this.bind_property('search-active', this._search_active_button, 'active',
                           GObject.BindingFlags.SYNC_CREATE |
                           GObject.BindingFlags.BIDIRECTIONAL);
        this.bind_property('search-active',
                           this._search_bar,
                           'search-mode-enabled',
                           GObject.BindingFlags.SYNC_CREATE |
                           GObject.BindingFlags.BIDIRECTIONAL);
        this._search_bar.connect_entry(this._search_entry);
        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._categoryListView =
            new CategoryList.CategoryListView({ vexpand: true });
        let scroll = new Gtk.ScrolledWindow({
            hscrollbar_policy: Gtk.PolicyType.NEVER,
            hexpand: false,
        });
        scroll.add(this._categoryListView);
        this._sidebar_grid.add(scroll);

        this._mainView = new MainView({
            categoryListView: this._categoryListView
        });

        this._main_hbox.pack_start(this._mainView, true, true, 0);
        this._main_grid.show_all();

        // Due to limitations of gobject-introspection wrt GdkEvent
        // and GdkEventKey, this needs to be a signal handler
        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;
    },

    set search_active(v) {
        if (this._searchActive == v)
            return;

        this._searchActive = v;

        if (this._searchActive) {
            let categoryList = this._categoryListView.get_visible_child();
            categoryList.unselect_all();
        }

        this.notify('search-active');
    },

    _handleSearchChanged: function(entry) {
        let text = entry.get_text().replace(/^\s+|\s+$/g, '');
        let keywords = text == '' ? [] : text.split(/\s+/);
        keywords = keywords.map(String.toUpperCase);
        if (keywords != this._searchKeywords) {
            this._mainView.cancelSearch();
            this._searchKeywords = keywords;
            if (this._searchKeywords.length > 0)
                this._mainView.searchByKeywords(this._searchKeywords);
        }
        return true;
    },

    _handleKeyPress: function(self, event) {
        if (this._menu_popover.visible)
            return false;
        return this._search_bar.handle_event(event);
    },

    _about: function() {
        let aboutDialog = new Gtk.AboutDialog(
            { artists: [ 'Allan Day <allanpday@gmail.com>',
                         'Jakub Steiner <jimmac@gmail.com>' ],
              authors: [ 'Daiki Ueno <dueno@src.gnome.org>',
                         'Giovanni Campagna <scampa.giovanni@gmail.com>' ],
              // TRANSLATORS: put your names here, one name per line.
              translator_credits: _("translator-credits"),
              program_name: _("GNOME Characters"),
              comments: _("Character Map"),
              copyright: 'Copyright 2014-2018 Daiki Ueno',
              license_type: Gtk.License.GPL_2_0,
              logo_icon_name: 'gnome-characters',
              version: pkg.version,
              // website: 'https://wiki.gnome.org/Design/Apps/CharacterMap',
              wrap_license: true,
              modal: true,
              transient_for: this
            });

        aboutDialog.show();
        aboutDialog.connect('response', function() {
            aboutDialog.destroy();
        });
    },

    _updateTitle: function(title) {
        if (this._mainView.filterFontFamily) {
            this._main_headerbar.title =
                _("%s (%s only)").format(Gettext.gettext(title),
                                         this._mainView.filterFontFamily);
        } else {
            this._main_headerbar.title = Gettext.gettext(title);
        }
    },

    _category: function(action, v) {
        this.search_active = false;

        let [name, length] = v.get_string()

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

    _filterFont: function(action, v) {
        let [family, length] = v.get_string()
        if (family == 'None')
            family = null;
        this._mainView.filterFontFamily = family;
        this._updateTitle(this._mainView.visible_child.title);
        this._menu_popover.hide();
    },

    _find: function() {
        this.search_active = !this.search_active;
    },

    setSearchKeywords: function(keywords) {
        this.search_active = keywords.length > 0;
        this._search_entry.set_text(keywords.join(' '));
    }
});

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', '', '',
            GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
            0, GLib.MAXUINT32, 100)
    },

    get max_recent_characters() {
        return this._maxRecentCharacters;
    },

    set max_recent_characters(v) {
        this._maxRecentCharacters = v;
        if (this.recentCharacters.length > this._maxRecentCharacters)
            this.recentCharacters = this.recentCharacters.slice(
                0, this._maxRecentCharacters);
    },

    get filterFontFamily() {
        return this._filterFontFamily;
    },

    set filterFontFamily(family) {
        this._filterFontFamily = family;
        this._fontFilter.setFilterFont(this._filterFontFamily);
    },

    _init: function(params) {
        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._recentCharacterLists = {};
        this._categoryListView = filtered.categoryListView;

        let characterList;
        let categories = this._categoryListView.getCategoryList();
        let recentBox = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL,
                                      hexpand: true, vexpand: false });

        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,
                // TRANSLATORS: %s will be either 'emojis' or 'letters'
                _('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_start(separator, false, false, 0);
            }
            recentBox.pack_start(characterList, false, false, 0);
        }
        let scroll = new Gtk.ScrolledWindow({
            hscrollbar_policy: Gtk.PolicyType.NEVER,
            hexpand: false,
        });
        scroll.add(recentBox);
        // FIXME: Can't use GtkContainer.child_get_property.
        scroll.title = _('Recently Used');
        this.add_titled(scroll, 'recent', scroll.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
        let recentCharacters = Main.settings.get_value('recent-characters');
        this.recentCharacters = recentCharacters.get_strv();
        this._maxRecentCharacters = 100;
        Main.settings.bind('max-recent-characters', this,
                           'max-recent-characters',
                           Gio.SettingsBindFlags.DEFAULT);
    },

    _createCharacterList: function(name, accessible_name) {
        let characterList = new CharacterList.CharacterListView({
            fontFilter: this._fontFilter
        });
        characterList.get_accessible().accessible_name = accessible_name;
        characterList.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;
    },

    searchByKeywords: function(keywords) {
        this.visible_child_name = 'search-result';
        this.visible_child.searchByKeywords(keywords);
    },

    cancelSearch: function() {
        let characterList = this.get_child_by_name('search-result');
        characterList.cancelSearch();
    },

    setPage: function(category) {
        if (category.name == 'recent') {
            if (this.recentCharacters.length == 0)
                this.visible_child_name = 'empty-recent';
            else {
                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;
        }
    },

    addToRecent: function(uc) {
        if (this.recentCharacters.indexOf(uc) < 0) {
            this.recentCharacters.unshift(uc);
            if (this.recentCharacters.length > this._maxRecentCharacters)
                this.recentCharacters = this.recentCharacters.slice(
                    0, this._maxRecentCharacters);
            Main.settings.set_value(
                'recent-characters',
                GLib.Variant.new_strv(this.recentCharacters));
        }
    },

    _addToRecent: function(widget, uc) {
        this.addToRecent(uc);
    },

    _handleCharacterSelected: function(widget, uc) {
        let dialog = new Character.CharacterDialog({
            character: uc,
            modal: true,
            transient_for: this.get_toplevel(),
            fontDescription: this._fontFilter.fontDescription
        });

        dialog.show();
        dialog.connect('character-copied',
                       Lang.bind(this, this._addToRecent));
        dialog.connect('response', function(self, response_id) {
            if (response_id == Gtk.ResponseType.CLOSE)
                dialog.destroy();
        });
    }
});
(uuay)