[gnome-shell] Implement input source switching



commit 8589bfb62ea4fa494bba99f1a58bf648bedbaacc
Author: Rui Matos <tiagomatos gmail com>
Date:   Thu Jun 5 18:47:48 2014 +0200

    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 |  136 +++++++++++++++++++++++++++++++++++---------
 js/ui/status/keyboard.js   |   63 ++++++++++++++++-----
 3 files changed, 174 insertions(+), 42 deletions(-)
---
diff --git a/js/misc/ibusManager.js b/js/misc/ibusManager.js
index 40a33a6..9571e0b 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 dc14af1..acc7eec 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
+    // 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,
 
-    _BUS_NAME: 'org.gnome.SettingsDaemon.Keyboard',
-    _OBJECT_PATH: '/org/gnome/SettingsDaemon/Keyboard',
+    _init: function() {
+        this._xkbInfo = getXkbInfo();
+        this._current = null;
+        this._localeLayoutInfo = this._getLocaleLayout();
+        this._layoutInfos = {};
+    },
 
-    _INTERFACE: '\
-        <node> \
-        <interface name="org.gnome.SettingsDaemon.Keyboard"> \
-            <method name="SetInputSource"> \
-                <arg type="u" direction="in" /> \
-            </method> \
-        </interface> \
-        </node>',
+    _applyLayoutGroup: function(group) {
+        let options = this._buildOptionsString();
+        let [layouts, variants] = this._buildGroupStrings(group);
+        Meta.get_backend().set_keymap(layouts, variants, options);
+    },
 
-    _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;
+    _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;
     },
 
-    SetInputSource: function(is) {
-        holdKeyboard();
-        this._proxy.SetInputSourceRemote(is.index, releaseKeyboard);
+    _buildOptionsString: function() {
+        let options = this._xkbOptions.join(',');
+        return options;
     }
 });
diff --git a/js/ui/status/keyboard.js b/js/ui/status/keyboard.js
index 46fb77d..5f8f5e6 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) {


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]