Edit file File name : org.gnome.Characters.src.gresource Content :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)Save