From 8589bfb62ea4fa494bba99f1a58bf648bedbaacc Mon Sep 17 00:00:00 2001 From: Rui Matos Date: Thu, 5 Jun 2014 18:47:48 +0200 Subject: [PATCH] Implement input source switching Instead of calling out to gnome-settings-daemon we'll just implement the switching logic ourselves and use mutter APIs that allow this functionality to work both in X sessions and when we're a Wayland compositor. Switching IBus engines is done transparently as well just like g-s-d used to do. https://bugzilla.gnome.org/show_bug.cgi?id=736435 --- js/misc/ibusManager.js | 17 ++++- js/misc/keyboardManager.js | 138 +++++++++++++++++++++++++++++-------- js/ui/status/keyboard.js | 63 +++++++++++++---- 3 files changed, 175 insertions(+), 43 deletions(-) diff --git a/js/misc/ibusManager.js b/js/misc/ibusManager.js index 40a33a642..9571e0b44 100644 --- a/js/misc/ibusManager.js +++ b/js/misc/ibusManager.js @@ -25,6 +25,10 @@ function getIBusManager() { const IBusManager = new Lang.Class({ Name: 'IBusManager', + // This is the longest we'll keep the keyboard frozen until an input + // source is active. + _MAX_INPUT_SOURCE_ACTIVATION_TIME: 4000, // ms + _init: function() { if (!IBus) return; @@ -160,6 +164,17 @@ const IBusManager = new Lang.Class({ return null; return this._engines[id]; - } + }, + + setEngine: function(id, callback) { + if (!IBus || !this._ready || id == this._currentEngineName) { + if (callback) + callback(); + return; + } + + this._ibus.set_global_engine_async(id, this._MAX_INPUT_SOURCE_ACTIVATION_TIME, + null, callback); + }, }); Signals.addSignalMethods(IBusManager.prototype); diff --git a/js/misc/keyboardManager.js b/js/misc/keyboardManager.js index dc14af1bb..acc7eecc9 100644 --- a/js/misc/keyboardManager.js +++ b/js/misc/keyboardManager.js @@ -1,11 +1,16 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; const GnomeDesktop = imports.gi.GnomeDesktop; const Lang = imports.lang; +const Meta = imports.gi.Meta; const Main = imports.ui.main; +const DEFAULT_LOCALE = 'en_US'; +const DEFAULT_LAYOUT = 'us'; +const DEFAULT_VARIANT = ''; + let _xkbInfo = null; function getXkbInfo() { @@ -36,36 +41,113 @@ function holdKeyboard() { const KeyboardManager = new Lang.Class({ Name: 'KeyboardManager', - // This is the longest we'll keep the keyboard frozen until an input - // source is active. - _MAX_INPUT_SOURCE_ACTIVATION_TIME: 4000, // ms - - _BUS_NAME: 'org.gnome.SettingsDaemon.Keyboard', - _OBJECT_PATH: '/org/gnome/SettingsDaemon/Keyboard', - - _INTERFACE: '\ - \ - \ - \ - \ - \ - \ - ', + // The XKB protocol doesn't allow for more that 4 layouts in a + // keymap. Wayland doesn't impose this limit and libxkbcommon can + // handle up to 32 layouts but since we need to support X clients + // even as a Wayland compositor, we can't bump this. + MAX_LAYOUTS_PER_GROUP: 4, _init: function() { - let Proxy = Gio.DBusProxy.makeProxyWrapper(this._INTERFACE); - this._proxy = new Proxy(Gio.DBus.session, - this._BUS_NAME, - this._OBJECT_PATH, - function(proxy, error) { - if (error) - log(error.message); - }); - this._proxy.g_default_timeout = this._MAX_INPUT_SOURCE_ACTIVATION_TIME; + this._xkbInfo = getXkbInfo(); + this._current = null; + this._localeLayoutInfo = this._getLocaleLayout(); + this._layoutInfos = {}; }, - SetInputSource: function(is) { - holdKeyboard(); - this._proxy.SetInputSourceRemote(is.index, releaseKeyboard); + _applyLayoutGroup: function(group) { + let options = this._buildOptionsString(); + let [layouts, variants] = this._buildGroupStrings(group); + Meta.get_backend().set_keymap(layouts, variants, options); + }, + + _applyLayoutGroupIndex: function(idx) { + Meta.get_backend().lock_layout_group(idx); + }, + + apply: function(id) { + let info = this._layoutInfos[id]; + if (!info) + return; + + if (this._current && this._current.group == info.group) { + if (this._current.groupIndex != info.groupIndex) + this._applyLayoutGroupIndex(info.groupIndex); + } else { + this._applyLayoutGroup(info.group); + this._applyLayoutGroupIndex(info.groupIndex); + } + + this._current = info; + }, + + reapply: function() { + if (!this._current) + return; + + this._applyLayoutGroup(this._current.group); + this._applyLayoutGroupIndex(this._current.groupIndex); + }, + + setUserLayouts: function(ids) { + this._current = null; + this._layoutInfos = {}; + + for (let i = 0; i < ids.length; ++i) { + let [found, , , _layout, _variant] = this._xkbInfo.get_layout_info(ids[i]); + if (found) + this._layoutInfos[ids[i]] = { id: ids[i], layout: _layout, variant: _variant }; + } + + let i = 0; + let group = []; + for (let id in this._layoutInfos) { + // We need to leave one slot on each group free so that we + // can add a layout containing the symbols for the + // language used in UI strings to ensure that toolkits can + // handle mnemonics like Alt+ะค even if the user is + // actually typing in a different layout. + let groupIndex = i % (this.MAX_LAYOUTS_PER_GROUP - 1); + if (groupIndex == 0) + group = []; + + let info = this._layoutInfos[id]; + group[groupIndex] = info; + info.group = group; + info.groupIndex = groupIndex; + + i += 1; + } + }, + + _getLocaleLayout: function() { + let locale = GLib.get_language_names()[0]; + if (locale.indexOf('_') == -1) + locale = DEFAULT_LOCALE; + + let [found, , id] = GnomeDesktop.get_input_source_from_locale(locale); + if (!found) + [, , id] = GnomeDesktop.get_input_source_from_locale(DEFAULT_LOCALE); + + let [found, , , _layout, _variant] = this._xkbInfo.get_layout_info(id); + if (found) + return { layout: _layout, variant: _variant }; + else + return { layout: DEFAULT_LAYOUT, variant: DEFAULT_VARIANT }; + }, + + _buildGroupStrings: function(_group) { + let group = _group.concat(this._localeLayoutInfo); + let layouts = group.map(function(g) { return g.layout; }).join(','); + let variants = group.map(function(g) { return g.variant; }).join(','); + return [layouts, variants]; + }, + + setKeyboardOptions: function(options) { + this._xkbOptions = options; + }, + + _buildOptionsString: function() { + let options = this._xkbOptions.join(','); + return options; } }); diff --git a/js/ui/status/keyboard.js b/js/ui/status/keyboard.js index 46fb77d21..5f8f5e6d2 100644 --- a/js/ui/status/keyboard.js +++ b/js/ui/status/keyboard.js @@ -19,8 +19,8 @@ const SwitcherPopup = imports.ui.switcherPopup; const Util = imports.misc.util; const DESKTOP_INPUT_SOURCES_SCHEMA = 'org.gnome.desktop.input-sources'; -const KEY_CURRENT_INPUT_SOURCE = 'current'; const KEY_INPUT_SOURCES = 'sources'; +const KEY_KEYBOARD_OPTIONS = 'xkb-options'; const INPUT_SOURCE_TYPE_XKB = 'xkb'; const INPUT_SOURCE_TYPE_IBUS = 'ibus'; @@ -51,6 +51,8 @@ const InputSource = new Lang.Class({ this.index = index; this.properties = null; + + this.xkbId = this._getXkbId(); }, get shortName() { @@ -65,6 +67,17 @@ const InputSource = new Lang.Class({ activate: function() { this.emit('activate'); }, + + _getXkbId: function() { + let engineDesc = IBusManager.getIBusManager().getEngineDesc(this.id); + if (!engineDesc) + return this.id; + + if (engineDesc.variant && engineDesc.variant.length > 0) + return engineDesc.layout + '+' + engineDesc.variant; + else + return engineDesc.layout; + } }); Signals.addSignalMethods(InputSource.prototype); @@ -159,10 +172,11 @@ const InputSourceManager = new Lang.Class({ Shell.KeyBindingMode.ALL, Lang.bind(this, this._switchInputSource)); this._settings = new Gio.Settings({ schema_id: DESKTOP_INPUT_SOURCES_SCHEMA }); - this._settings.connect('changed::' + KEY_CURRENT_INPUT_SOURCE, Lang.bind(this, this._currentInputSourceChanged)); this._settings.connect('changed::' + KEY_INPUT_SOURCES, Lang.bind(this, this._inputSourcesChanged)); + this._settings.connect('changed::' + KEY_KEYBOARD_OPTIONS, Lang.bind(this, this._keyboardOptionsChanged)); this._xkbInfo = KeyboardManager.getXkbInfo(); + this._keyboardManager = KeyboardManager.getKeyboardManager(); this._ibusReady = false; this._ibusManager = IBusManager.getIBusManager(); @@ -170,8 +184,6 @@ const InputSourceManager = new Lang.Class({ this._ibusManager.connect('properties-registered', Lang.bind(this, this._ibusPropertiesRegistered)); this._ibusManager.connect('property-updated', Lang.bind(this, this._ibusPropertyUpdated)); - this._keyboardManager = KeyboardManager.getKeyboardManager(); - global.display.connect('modifiers-accelerator-activated', Lang.bind(this, this._modifiersSwitcher)); this._sourcesPerWindow = false; @@ -183,6 +195,7 @@ const InputSourceManager = new Lang.Class({ }, reload: function() { + this._keyboardManager.setKeyboardOptions(this._settings.get_strv(KEY_KEYBOARD_OPTIONS)); this._inputSourcesChanged(); }, @@ -237,10 +250,12 @@ const InputSourceManager = new Lang.Class({ popup.destroy(); }, - _currentInputSourceChanged: function() { - let newSourceIndex = this._settings.get_uint(KEY_CURRENT_INPUT_SOURCE); - let newSource = this._inputSources[newSourceIndex]; + _keyboardOptionsChanged: function() { + this._keyboardManager.setKeyboardOptions(this._settings.get_strv(KEY_KEYBOARD_OPTIONS)); + this._keyboardManager.reapply(); + }, + _currentInputSourceChanged: function(newSource) { let oldSource; [oldSource, this._currentSource] = [this._currentSource, newSource]; @@ -256,13 +271,32 @@ const InputSourceManager = new Lang.Class({ this._changePerWindowSource(); }, + _activateInputSource: function(is) { + KeyboardManager.holdKeyboard(); + this._keyboardManager.apply(is.xkbId); + + // All the "xkb:..." IBus engines simply "echo" back symbols, + // despite their naming implying differently, so we always set + // one in order for XIM applications to work given that we set + // XMODIFIERS=@im=ibus in the first place so that they can + // work without restarting when/if the user adds an IBus input + // source. + let engine; + if (is.type == INPUT_SOURCE_TYPE_IBUS) + engine = is.id; + else + engine = 'xkb:us::eng'; + + this._ibusManager.setEngine(engine, KeyboardManager.releaseKeyboard); + this._currentInputSourceChanged(is); + }, + _inputSourcesChanged: function() { let sources = this._settings.get_value(KEY_INPUT_SOURCES); let nSources = sources.n_children(); this._inputSources = {}; this._ibusSources = {}; - this._currentSource = null; let inputSourcesByShortName = {}; @@ -294,9 +328,7 @@ const InputSourceManager = new Lang.Class({ let is = new InputSource(type, id, displayName, shortName, i); - is.connect('activate', Lang.bind(this, function() { - this._keyboardManager.SetInputSource(is); - })); + is.connect('activate', Lang.bind(this, this._activateInputSource)); if (!(is.shortName in inputSourcesByShortName)) inputSourcesByShortName[is.shortName] = []; @@ -322,6 +354,8 @@ const InputSourceManager = new Lang.Class({ for (let i in this._inputSources) sourcesList.push(this._inputSources[i]); + this._keyboardManager.setUserLayouts(sourcesList.map(function(x) { return x.xkbId; })); + let mruSources = []; for (let i = 0; i < this._mruSources.length; i++) { for (let j = 0; j < sourcesList.length; j++) @@ -333,7 +367,8 @@ const InputSourceManager = new Lang.Class({ } this._mruSources = mruSources.concat(sourcesList); - this._currentInputSourceChanged(); + if (this._mruSources.length > 0) + this._mruSources[0].activate(); }, _makeEngineShortName: function(engineDesc) { @@ -356,7 +391,7 @@ const InputSourceManager = new Lang.Class({ source.properties = props; if (source == this._currentSource) - this._currentInputSourceChanged(); + this.emit('current-source-changed', null); }, _ibusPropertyUpdated: function(im, engineName, prop) { @@ -366,7 +401,7 @@ const InputSourceManager = new Lang.Class({ if (this._updateSubProperty(source.properties, prop) && source == this._currentSource) - this._currentInputSourceChanged(); + this.emit('current-source-changed', null); }, _updateSubProperty: function(props, prop) {