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
This commit is contained in:
parent
6a36a68f32
commit
8589bfb62e
@ -25,6 +25,10 @@ function getIBusManager() {
|
|||||||
const IBusManager = new Lang.Class({
|
const IBusManager = new Lang.Class({
|
||||||
Name: 'IBusManager',
|
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() {
|
_init: function() {
|
||||||
if (!IBus)
|
if (!IBus)
|
||||||
return;
|
return;
|
||||||
@ -160,6 +164,17 @@ const IBusManager = new Lang.Class({
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
return this._engines[id];
|
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);
|
Signals.addSignalMethods(IBusManager.prototype);
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
// -*- 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 GnomeDesktop = imports.gi.GnomeDesktop;
|
||||||
const Lang = imports.lang;
|
const Lang = imports.lang;
|
||||||
|
const Meta = imports.gi.Meta;
|
||||||
|
|
||||||
const Main = imports.ui.main;
|
const Main = imports.ui.main;
|
||||||
|
|
||||||
|
const DEFAULT_LOCALE = 'en_US';
|
||||||
|
const DEFAULT_LAYOUT = 'us';
|
||||||
|
const DEFAULT_VARIANT = '';
|
||||||
|
|
||||||
let _xkbInfo = null;
|
let _xkbInfo = null;
|
||||||
|
|
||||||
function getXkbInfo() {
|
function getXkbInfo() {
|
||||||
@ -36,36 +41,113 @@ function holdKeyboard() {
|
|||||||
const KeyboardManager = new Lang.Class({
|
const KeyboardManager = new Lang.Class({
|
||||||
Name: 'KeyboardManager',
|
Name: 'KeyboardManager',
|
||||||
|
|
||||||
// This is the longest we'll keep the keyboard frozen until an input
|
// The XKB protocol doesn't allow for more that 4 layouts in a
|
||||||
// source is active.
|
// keymap. Wayland doesn't impose this limit and libxkbcommon can
|
||||||
_MAX_INPUT_SOURCE_ACTIVATION_TIME: 4000, // ms
|
// handle up to 32 layouts but since we need to support X clients
|
||||||
|
// even as a Wayland compositor, we can't bump this.
|
||||||
_BUS_NAME: 'org.gnome.SettingsDaemon.Keyboard',
|
MAX_LAYOUTS_PER_GROUP: 4,
|
||||||
_OBJECT_PATH: '/org/gnome/SettingsDaemon/Keyboard',
|
|
||||||
|
|
||||||
_INTERFACE: '\
|
|
||||||
<node> \
|
|
||||||
<interface name="org.gnome.SettingsDaemon.Keyboard"> \
|
|
||||||
<method name="SetInputSource"> \
|
|
||||||
<arg type="u" direction="in" /> \
|
|
||||||
</method> \
|
|
||||||
</interface> \
|
|
||||||
</node>',
|
|
||||||
|
|
||||||
_init: function() {
|
_init: function() {
|
||||||
let Proxy = Gio.DBusProxy.makeProxyWrapper(this._INTERFACE);
|
this._xkbInfo = getXkbInfo();
|
||||||
this._proxy = new Proxy(Gio.DBus.session,
|
this._current = null;
|
||||||
this._BUS_NAME,
|
this._localeLayoutInfo = this._getLocaleLayout();
|
||||||
this._OBJECT_PATH,
|
this._layoutInfos = {};
|
||||||
function(proxy, error) {
|
|
||||||
if (error)
|
|
||||||
log(error.message);
|
|
||||||
});
|
|
||||||
this._proxy.g_default_timeout = this._MAX_INPUT_SOURCE_ACTIVATION_TIME;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
SetInputSource: function(is) {
|
_applyLayoutGroup: function(group) {
|
||||||
holdKeyboard();
|
let options = this._buildOptionsString();
|
||||||
this._proxy.SetInputSourceRemote(is.index, releaseKeyboard);
|
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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -19,8 +19,8 @@ const SwitcherPopup = imports.ui.switcherPopup;
|
|||||||
const Util = imports.misc.util;
|
const Util = imports.misc.util;
|
||||||
|
|
||||||
const DESKTOP_INPUT_SOURCES_SCHEMA = 'org.gnome.desktop.input-sources';
|
const DESKTOP_INPUT_SOURCES_SCHEMA = 'org.gnome.desktop.input-sources';
|
||||||
const KEY_CURRENT_INPUT_SOURCE = 'current';
|
|
||||||
const KEY_INPUT_SOURCES = 'sources';
|
const KEY_INPUT_SOURCES = 'sources';
|
||||||
|
const KEY_KEYBOARD_OPTIONS = 'xkb-options';
|
||||||
|
|
||||||
const INPUT_SOURCE_TYPE_XKB = 'xkb';
|
const INPUT_SOURCE_TYPE_XKB = 'xkb';
|
||||||
const INPUT_SOURCE_TYPE_IBUS = 'ibus';
|
const INPUT_SOURCE_TYPE_IBUS = 'ibus';
|
||||||
@ -51,6 +51,8 @@ const InputSource = new Lang.Class({
|
|||||||
this.index = index;
|
this.index = index;
|
||||||
|
|
||||||
this.properties = null;
|
this.properties = null;
|
||||||
|
|
||||||
|
this.xkbId = this._getXkbId();
|
||||||
},
|
},
|
||||||
|
|
||||||
get shortName() {
|
get shortName() {
|
||||||
@ -65,6 +67,17 @@ const InputSource = new Lang.Class({
|
|||||||
activate: function() {
|
activate: function() {
|
||||||
this.emit('activate');
|
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);
|
Signals.addSignalMethods(InputSource.prototype);
|
||||||
|
|
||||||
@ -159,10 +172,11 @@ const InputSourceManager = new Lang.Class({
|
|||||||
Shell.KeyBindingMode.ALL,
|
Shell.KeyBindingMode.ALL,
|
||||||
Lang.bind(this, this._switchInputSource));
|
Lang.bind(this, this._switchInputSource));
|
||||||
this._settings = new Gio.Settings({ schema_id: DESKTOP_INPUT_SOURCES_SCHEMA });
|
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_INPUT_SOURCES, Lang.bind(this, this._inputSourcesChanged));
|
||||||
|
this._settings.connect('changed::' + KEY_KEYBOARD_OPTIONS, Lang.bind(this, this._keyboardOptionsChanged));
|
||||||
|
|
||||||
this._xkbInfo = KeyboardManager.getXkbInfo();
|
this._xkbInfo = KeyboardManager.getXkbInfo();
|
||||||
|
this._keyboardManager = KeyboardManager.getKeyboardManager();
|
||||||
|
|
||||||
this._ibusReady = false;
|
this._ibusReady = false;
|
||||||
this._ibusManager = IBusManager.getIBusManager();
|
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('properties-registered', Lang.bind(this, this._ibusPropertiesRegistered));
|
||||||
this._ibusManager.connect('property-updated', Lang.bind(this, this._ibusPropertyUpdated));
|
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));
|
global.display.connect('modifiers-accelerator-activated', Lang.bind(this, this._modifiersSwitcher));
|
||||||
|
|
||||||
this._sourcesPerWindow = false;
|
this._sourcesPerWindow = false;
|
||||||
@ -183,6 +195,7 @@ const InputSourceManager = new Lang.Class({
|
|||||||
},
|
},
|
||||||
|
|
||||||
reload: function() {
|
reload: function() {
|
||||||
|
this._keyboardManager.setKeyboardOptions(this._settings.get_strv(KEY_KEYBOARD_OPTIONS));
|
||||||
this._inputSourcesChanged();
|
this._inputSourcesChanged();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -237,10 +250,12 @@ const InputSourceManager = new Lang.Class({
|
|||||||
popup.destroy();
|
popup.destroy();
|
||||||
},
|
},
|
||||||
|
|
||||||
_currentInputSourceChanged: function() {
|
_keyboardOptionsChanged: function() {
|
||||||
let newSourceIndex = this._settings.get_uint(KEY_CURRENT_INPUT_SOURCE);
|
this._keyboardManager.setKeyboardOptions(this._settings.get_strv(KEY_KEYBOARD_OPTIONS));
|
||||||
let newSource = this._inputSources[newSourceIndex];
|
this._keyboardManager.reapply();
|
||||||
|
},
|
||||||
|
|
||||||
|
_currentInputSourceChanged: function(newSource) {
|
||||||
let oldSource;
|
let oldSource;
|
||||||
[oldSource, this._currentSource] = [this._currentSource, newSource];
|
[oldSource, this._currentSource] = [this._currentSource, newSource];
|
||||||
|
|
||||||
@ -256,13 +271,32 @@ const InputSourceManager = new Lang.Class({
|
|||||||
this._changePerWindowSource();
|
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() {
|
_inputSourcesChanged: function() {
|
||||||
let sources = this._settings.get_value(KEY_INPUT_SOURCES);
|
let sources = this._settings.get_value(KEY_INPUT_SOURCES);
|
||||||
let nSources = sources.n_children();
|
let nSources = sources.n_children();
|
||||||
|
|
||||||
this._inputSources = {};
|
this._inputSources = {};
|
||||||
this._ibusSources = {};
|
this._ibusSources = {};
|
||||||
this._currentSource = null;
|
|
||||||
|
|
||||||
let inputSourcesByShortName = {};
|
let inputSourcesByShortName = {};
|
||||||
|
|
||||||
@ -294,9 +328,7 @@ const InputSourceManager = new Lang.Class({
|
|||||||
|
|
||||||
let is = new InputSource(type, id, displayName, shortName, i);
|
let is = new InputSource(type, id, displayName, shortName, i);
|
||||||
|
|
||||||
is.connect('activate', Lang.bind(this, function() {
|
is.connect('activate', Lang.bind(this, this._activateInputSource));
|
||||||
this._keyboardManager.SetInputSource(is);
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!(is.shortName in inputSourcesByShortName))
|
if (!(is.shortName in inputSourcesByShortName))
|
||||||
inputSourcesByShortName[is.shortName] = [];
|
inputSourcesByShortName[is.shortName] = [];
|
||||||
@ -322,6 +354,8 @@ const InputSourceManager = new Lang.Class({
|
|||||||
for (let i in this._inputSources)
|
for (let i in this._inputSources)
|
||||||
sourcesList.push(this._inputSources[i]);
|
sourcesList.push(this._inputSources[i]);
|
||||||
|
|
||||||
|
this._keyboardManager.setUserLayouts(sourcesList.map(function(x) { return x.xkbId; }));
|
||||||
|
|
||||||
let mruSources = [];
|
let mruSources = [];
|
||||||
for (let i = 0; i < this._mruSources.length; i++) {
|
for (let i = 0; i < this._mruSources.length; i++) {
|
||||||
for (let j = 0; j < sourcesList.length; j++)
|
for (let j = 0; j < sourcesList.length; j++)
|
||||||
@ -333,7 +367,8 @@ const InputSourceManager = new Lang.Class({
|
|||||||
}
|
}
|
||||||
this._mruSources = mruSources.concat(sourcesList);
|
this._mruSources = mruSources.concat(sourcesList);
|
||||||
|
|
||||||
this._currentInputSourceChanged();
|
if (this._mruSources.length > 0)
|
||||||
|
this._mruSources[0].activate();
|
||||||
},
|
},
|
||||||
|
|
||||||
_makeEngineShortName: function(engineDesc) {
|
_makeEngineShortName: function(engineDesc) {
|
||||||
@ -356,7 +391,7 @@ const InputSourceManager = new Lang.Class({
|
|||||||
source.properties = props;
|
source.properties = props;
|
||||||
|
|
||||||
if (source == this._currentSource)
|
if (source == this._currentSource)
|
||||||
this._currentInputSourceChanged();
|
this.emit('current-source-changed', null);
|
||||||
},
|
},
|
||||||
|
|
||||||
_ibusPropertyUpdated: function(im, engineName, prop) {
|
_ibusPropertyUpdated: function(im, engineName, prop) {
|
||||||
@ -366,7 +401,7 @@ const InputSourceManager = new Lang.Class({
|
|||||||
|
|
||||||
if (this._updateSubProperty(source.properties, prop) &&
|
if (this._updateSubProperty(source.properties, prop) &&
|
||||||
source == this._currentSource)
|
source == this._currentSource)
|
||||||
this._currentInputSourceChanged();
|
this.emit('current-source-changed', null);
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateSubProperty: function(props, prop) {
|
_updateSubProperty: function(props, prop) {
|
||||||
|
Loading…
Reference in New Issue
Block a user