From 5914f225a2996652763bfd06a35ab3f9971df952 Mon Sep 17 00:00:00 2001 From: Carlos Garnacho Date: Tue, 5 Dec 2017 20:05:00 +0100 Subject: [PATCH] misc: Add InputMethod class This is a ClutterInputMethod implementation using IBus underneath. The input method will interact with the currently focused ClutterInputFocus, be it shell chrome or wayland clients through the text_input protocol. --- js/js-resources.gresource.xml | 1 + js/misc/inputMethod.js | 214 ++++++++++++++++++++++++++++++++++ js/ui/main.js | 5 + 3 files changed, 220 insertions(+) create mode 100644 js/misc/inputMethod.js diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index ba0d2f483..6834bf608 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -17,6 +17,7 @@ misc/gnomeSession.js misc/history.js misc/ibusManager.js + misc/inputMethod.js misc/jsParse.js misc/keyboardManager.js misc/loginManager.js diff --git a/js/misc/inputMethod.js b/js/misc/inputMethod.js new file mode 100644 index 000000000..1e25e22f9 --- /dev/null +++ b/js/misc/inputMethod.js @@ -0,0 +1,214 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +const Clutter = imports.gi.Clutter; +const IBus = imports.gi.IBus; +const Keyboard = imports.ui.status.keyboard; +const Lang = imports.lang; +const Signals = imports.signals; + +var InputMethod = new Lang.Class({ + Name: 'InputMethod', + Extends: Clutter.InputMethod, + + _init: function() { + this.parent(); + this._hints = 0; + this._purpose = 0; + this._enabled = true; + this._currentFocus = null; + this._ibus = IBus.Bus.new_async(); + this._ibus.connect('connected', Lang.bind(this, this._onConnected)); + this._ibus.connect('disconnected', Lang.bind(this, this._clear)); + this.connect('notify::can-show-preedit', Lang.bind(this, this._updateCapabilities)); + + this._inputSourceManager = Keyboard.getInputSourceManager(); + this._sourceChangedId = this._inputSourceManager.connect('current-source-changed', + Lang.bind(this, this._onSourceChanged)); + this._currentSource = this._inputSourceManager.currentSource; + + if (this._ibus.is_connected()) + this._onConnected(); + }, + + get currentFocus() { + return this._currentFocus; + }, + + _updateCapabilities: function() { + let caps = 0; + + if (this.can_show_preedit) + caps |= IBus.Capabilite.PREEDIT_TEXT; + + if (this._currentFocus) + caps |= IBus.Capabilite.FOCUS | IBus.Capabilite.SURROUNDING_TEXT; + else + caps |= IBus.Capabilite.PREEDIT_TEXT | IBus.Capabilite.AUXILIARY_TEXT | IBus.Capabilite.LOOKUP_TABLE | IBus.Capabilite.PROPERTY; + + if (this._context) + this._context.set_capabilities(caps); + }, + + _onSourceChanged: function() { + this._currentSource = this._inputSourceManager.currentSource; + }, + + _onConnected: function() { + this._ibus.create_input_context_async ('gnome-shell', -1, null, + Lang.bind(this, this._setContext)); + }, + + _setContext: function(bus, res) { + this._context = this._ibus.create_input_context_async_finish(res); + this._context.connect('enabled', Lang.bind(this, function () { this._enabled = true })); + this._context.connect('disabled', Lang.bind(this, function () { this._enabled = false })); + this._context.connect('commit-text', Lang.bind(this, this._onCommitText)); + this._context.connect('delete-surrounding-text', Lang.bind(this, this._onDeleteSurroundingText)); + this._context.connect('update-preedit-text', Lang.bind(this, this._onUpdatePreeditText)); + + this._updateCapabilities(); + }, + + _clear: function() { + this._context = null; + this._hints = 0; + this._purpose = 0; + this._enabled = false; + }, + + _emitRequestSurrounding: function() { + if (this._context.needs_surrounding_text()) + this.emit('request-surrounding'); + }, + + _onCommitText: function(context, text) { + this.commit(text.get_text()); + }, + + _onDeleteSurroundingText: function (context) { + this.delete_surrounding(); + }, + + _onUpdatePreeditText: function (context, text, pos, visible) { + let str = null; + if (visible && text != null) + str = text.get_text(); + + this.set_preedit_text(str, pos); + }, + + vfunc_focus_in: function(focus) { + this._currentFocus = focus; + if (this._context) { + this._context.focus_in(); + this._updateCapabilities(); + this._emitRequestSurrounding(); + } + }, + + vfunc_focus_out: function() { + this._currentFocus = null; + if (this._context) { + this._context.focus_out(); + this._updateCapabilities(); + } + + // Unset any preedit text + this.set_preedit_text(null, 0); + }, + + vfunc_reset: function() { + if (this._context) { + this._context.reset(); + this._emitRequestSurrounding(); + } + + // Unset any preedit text + this.set_preedit_text(null, 0); + }, + + vfunc_set_cursor_location: function(rect) { + if (this._context) { + this._context.set_cursor_location(rect.get_x(), rect.get_y(), + rect.get_width(), rect.get_height()); + this._emitRequestSurrounding(); + } + }, + + vfunc_set_surrounding: function(text, cursor, anchor) { + if (this._context) + this._context.set_surrounding_text(text, cursor, anchor); + }, + + vfunc_update_content_hints: function(hints) { + let ibusHints = 0; + if (hints & Clutter.InputContentHintFlags.COMPLETION) + ibusHints |= IBus.InputHints.WORD_COMPLETION; + if (hints & Clutter.InputContentHintFlags.SPELLCHECK) + ibusHints |= IBus.InputHints.SPELLCHECK; + if (hints & Clutter.InputContentHintFlags.AUTO_CAPITALIZATION) + ibusHints |= IBus.InputHints.UPPERCASE_SENTENCES; + if (hints & Clutter.InputContentHintFlags.LOWERCASE) + ibusHints |= IBus.InputHints.LOWERCASE; + if (hints & Clutter.InputContentHintFlags.UPPERCASE) + ibusHints |= IBus.InputHints.UPPERCASE_CHARS; + if (hints & Clutter.InputContentHintFlags.TITLECASE) + ibusHints |= IBus.InputHints.UPPERCASE_WORDS; + + this._hints = ibusHints; + if (this._context) + this._context.set_content_type(this._purpose, this._hints); + }, + + vfunc_update_content_purpose: function(purpose) { + let ibusPurpose = 0; + if (purpose == Clutter.InputContentPurpose.NORMAL) + ibusPurpose = IBus.InputPurpose.FREE_FORM; + else if (purpose == Clutter.InputContentPurpose.ALPHA) + ibusPurpose = IBus.InputPurpose.ALPHA; + else if (purpose == Clutter.InputContentPurpose.DIGITS) + ibusPurpose = IBus.InputPurpose.DIGITS; + else if (purpose == Clutter.InputContentPurpose.NUMBER) + ibusPurpose = IBus.InputPurpose.NUMBER; + else if (purpose == Clutter.InputContentPurpose.PHONE) + ibusPurpose = IBus.InputPurpose.PHONE; + else if (purpose == Clutter.InputContentPurpose.URL) + ibusPurpose = IBus.InputPurpose.URL; + else if (purpose == Clutter.InputContentPurpose.EMAIL) + ibusPurpose = IBus.InputPurpose.EMAIL; + else if (purpose == Clutter.InputContentPurpose.NAME) + ibusPurpose = IBus.InputPurpose.NAME; + else if (purpose == Clutter.InputContentPurpose.PASSWORD) + ibusPurpose = IBus.InputPurpose.PASSWORD; + + this._purpose = ibusPurpose; + if (this._context) + this._context.set_content_type(this._purpose, this._hints); + }, + + vfunc_filter_key_event: function(event) { + if (!this._context || !this._enabled) + return false; + if (!this._currentSource || + this._currentSource.type == Keyboard.INPUT_SOURCE_TYPE_XKB) + return false; + + let state = event.get_state(); + if (state & IBus.ModifierType.IGNORED_MASK) + return false; + + if (event.type() == Clutter.EventType.KEY_RELEASE) + state |= IBus.ModifierType.RELEASE_MASK; + this._context.process_key_event_async(event.get_key_symbol(), + event.get_key_code() - 8, // Convert XKB keycodes to evcodes + state, -1, null, + Lang.bind(this, (context, res) => { + try { + let retval = context.process_key_event_async_finish(res); + this.notify_key_event(event, retval); + } catch (e) { + log('Error processing key on IM: ' + e.message); + } + })); + return true; + }, +}); diff --git a/js/ui/main.js b/js/ui/main.js index 0f53edf48..7c1214e0a 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -19,6 +19,7 @@ const EndSessionDialog = imports.ui.endSessionDialog; const Environment = imports.ui.environment; const ExtensionSystem = imports.ui.extensionSystem; const ExtensionDownloader = imports.ui.extensionDownloader; +const InputMethod = imports.misc.inputMethod; const Keyboard = imports.ui.keyboard; const MessageTray = imports.ui.messageTray; const ModalDialog = imports.ui.modalDialog; @@ -80,6 +81,7 @@ var xdndHandler = null; var keyboard = null; var layoutManager = null; var kbdA11yDialog = null; +var inputMethod = null; let _startDate; let _defaultCssStylesheet = null; let _cssStylesheet = null; @@ -173,6 +175,9 @@ function _initializeUI() { if (LoginManager.canLock()) screenShield = new ScreenShield.ScreenShield(); + inputMethod = new InputMethod.InputMethod(); + Clutter.get_default_backend().set_input_method(inputMethod); + messageTray = new MessageTray.MessageTray(); panel = new Panel.Panel(); keyboard = new Keyboard.Keyboard();