Mini Shell
GVariant � (
Ե ����� L � � KFv� � L � � {W�y � v � � ��$0 � L � � KP� � L � � c��3 � v � �2 ���� �2 L 3 3 ��.G 3 v 3 � �g� � v � �� +Ka� �� v ȣ `� �(�
`� v h� � ���� � 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.js j8 // -*- 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)