diff options
Diffstat (limited to '')
-rw-r--r-- | js/misc/ibusManager.js | 398 |
1 files changed, 398 insertions, 0 deletions
diff --git a/js/misc/ibusManager.js b/js/misc/ibusManager.js new file mode 100644 index 0000000..214db55 --- /dev/null +++ b/js/misc/ibusManager.js @@ -0,0 +1,398 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported getIBusManager */ + +const { Gio, GLib, IBus, Meta, Shell } = imports.gi; +const Signals = imports.misc.signals; +const BoxPointer = imports.ui.boxpointer; + +const IBusCandidatePopup = imports.ui.ibusCandidatePopup; + +Gio._promisify(IBus.Bus.prototype, + 'list_engines_async', 'list_engines_async_finish'); +Gio._promisify(IBus.Bus.prototype, + 'request_name_async', 'request_name_async_finish'); +Gio._promisify(IBus.Bus.prototype, + 'get_global_engine_async', 'get_global_engine_async_finish'); +Gio._promisify(IBus.Bus.prototype, + 'set_global_engine_async', 'set_global_engine_async_finish'); +Gio._promisify(Shell, 'util_systemd_unit_exists'); + +// Ensure runtime version matches +_checkIBusVersion(1, 5, 2); + +let _ibusManager = null; +const IBUS_SYSTEMD_SERVICE = 'org.freedesktop.IBus.session.GNOME.service'; + +const TYPING_BOOSTER_ENGINE = 'typing-booster'; +const IBUS_TYPING_BOOSTER_SCHEMA = 'org.freedesktop.ibus.engine.typing-booster'; +const KEY_EMOJIPREDICTIONS = 'emojipredictions'; +const KEY_DICTIONARY = 'dictionary'; +const KEY_INLINECOMPLETION = 'inlinecompletion'; +const KEY_INPUTMETHOD = 'inputmethod'; + +function _checkIBusVersion(requiredMajor, requiredMinor, requiredMicro) { + if ((IBus.MAJOR_VERSION > requiredMajor) || + (IBus.MAJOR_VERSION == requiredMajor && IBus.MINOR_VERSION > requiredMinor) || + (IBus.MAJOR_VERSION == requiredMajor && IBus.MINOR_VERSION == requiredMinor && + IBus.MICRO_VERSION >= requiredMicro)) + return; + + throw new Error(`Found IBus version ${ + IBus.MAJOR_VERSION}.${IBus.MINOR_VERSION}.${IBus.MINOR_VERSION} ` + + `but required is ${requiredMajor}.${requiredMinor}.${requiredMicro}`); +} + +function getIBusManager() { + if (_ibusManager == null) + _ibusManager = new IBusManager(); + return _ibusManager; +} + +var IBusManager = class extends Signals.EventEmitter { + constructor() { + super(); + + IBus.init(); + + // This is the longest we'll keep the keyboard frozen until an input + // source is active. + this._MAX_INPUT_SOURCE_ACTIVATION_TIME = 4000; // ms + this._PRELOAD_ENGINES_DELAY_TIME = 30; // sec + + + this._candidatePopup = new IBusCandidatePopup.CandidatePopup(); + + this._panelService = null; + this._engines = new Map(); + this._ready = false; + this._registerPropertiesId = 0; + this._currentEngineName = null; + this._preloadEnginesId = 0; + + this._ibus = IBus.Bus.new_async(); + this._ibus.connect('connected', this._onConnected.bind(this)); + this._ibus.connect('disconnected', this._clear.bind(this)); + // Need to set this to get 'global-engine-changed' emitions + this._ibus.set_watch_ibus_signal(true); + this._ibus.connect('global-engine-changed', this._engineChanged.bind(this)); + + this._queueSpawn(); + } + + async _ibusSystemdServiceExists() { + if (this._ibusIsSystemdService) + return true; + + try { + this._ibusIsSystemdService = + await Shell.util_systemd_unit_exists( + IBUS_SYSTEMD_SERVICE, null); + } catch (e) { + this._ibusIsSystemdService = false; + } + + return this._ibusIsSystemdService; + } + + async _queueSpawn() { + const isSystemdService = await this._ibusSystemdServiceExists(); + if (!isSystemdService) + this._spawn(Meta.is_wayland_compositor() ? [] : ['--xim']); + } + + _tryAppendEnv(env, varname) { + const value = GLib.getenv(varname); + if (value) + env.push(`${varname}=${value}`); + } + + _spawn(extraArgs = []) { + try { + const cmdLine = ['ibus-daemon', '--panel', 'disable', ...extraArgs]; + const launchContext = global.create_app_launch_context(0, -1); + const env = launchContext.get_environment(); + // Use DO_NOT_REAP_CHILD to avoid adouble-fork internally + // since ibus-daemon refuses to start with init as its parent. + const [success_, pid] = GLib.spawn_async( + null, cmdLine, env, + GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD, + () => { + try { + global.context.restore_rlimit_nofile(); + } catch (err) { + } + } + ); + GLib.child_watch_add( + GLib.PRIORITY_DEFAULT, + pid, + () => GLib.spawn_close_pid(pid) + ); + } catch (e) { + log(`Failed to launch ibus-daemon: ${e.message}`); + } + } + + async restartDaemon(extraArgs = []) { + const isSystemdService = await this._ibusSystemdServiceExists(); + if (!isSystemdService) + this._spawn(['-r', ...extraArgs]); + } + + _clear() { + if (this._cancellable) { + this._cancellable.cancel(); + this._cancellable = null; + } + + if (this._preloadEnginesId) { + GLib.source_remove(this._preloadEnginesId); + this._preloadEnginesId = 0; + } + + if (this._panelService) + this._panelService.destroy(); + + this._panelService = null; + this._candidatePopup.setPanelService(null); + this._engines.clear(); + this._ready = false; + this._registerPropertiesId = 0; + this._currentEngineName = null; + + this.emit('ready', false); + } + + _onConnected() { + this._cancellable = new Gio.Cancellable(); + this._initEngines(); + this._initPanelService(); + } + + async _initEngines() { + try { + const enginesList = + await this._ibus.list_engines_async(-1, this._cancellable); + for (let i = 0; i < enginesList.length; ++i) { + let name = enginesList[i].get_name(); + this._engines.set(name, enginesList[i]); + } + this._updateReadiness(); + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + return; + + logError(e); + this._clear(); + } + } + + async _initPanelService() { + try { + await this._ibus.request_name_async(IBus.SERVICE_PANEL, + IBus.BusNameFlag.REPLACE_EXISTING, -1, this._cancellable); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + logError(e); + this._clear(); + } + return; + } + + this._panelService = new IBus.PanelService({ + connection: this._ibus.get_connection(), + object_path: IBus.PATH_PANEL, + }); + this._candidatePopup.setPanelService(this._panelService); + this._panelService.connect('update-property', this._updateProperty.bind(this)); + this._panelService.connect('set-cursor-location', (ps, x, y, w, h) => { + let cursorLocation = { x, y, width: w, height: h }; + this.emit('set-cursor-location', cursorLocation); + }); + this._panelService.connect('focus-in', (panel, path) => { + if (!GLib.str_has_suffix(path, '/InputContext_1')) + this.emit('focus-in'); + }); + this._panelService.connect('focus-out', () => this.emit('focus-out')); + + try { + // IBus versions older than 1.5.10 have a bug which + // causes spurious set-content-type emissions when + // switching input focus that temporarily lose purpose + // and hints defeating its intended semantics and + // confusing users. We thus don't use it in that case. + _checkIBusVersion(1, 5, 10); + this._panelService.connect('set-content-type', this._setContentType.bind(this)); + } catch (e) { + } + this._updateReadiness(); + + try { + // If an engine is already active we need to get its properties + const engine = + await this._ibus.get_global_engine_async(-1, this._cancellable); + this._engineChanged(this._ibus, engine.get_name()); + } catch (e) { + } + } + + _updateReadiness() { + this._ready = this._engines.size > 0 && this._panelService != null; + this.emit('ready', this._ready); + } + + _engineChanged(bus, engineName) { + if (!this._ready) + return; + + this._currentEngineName = engineName; + this._candidatePopup.close(BoxPointer.PopupAnimation.NONE); + + if (this._registerPropertiesId != 0) + return; + + this._registerPropertiesId = + this._panelService.connect('register-properties', (p, props) => { + if (!props.get(0)) + return; + + this._panelService.disconnect(this._registerPropertiesId); + this._registerPropertiesId = 0; + + this.emit('properties-registered', this._currentEngineName, props); + }); + } + + _updateProperty(panel, prop) { + this.emit('property-updated', this._currentEngineName, prop); + } + + _setContentType(panel, purpose, hints) { + this.emit('set-content-type', purpose, hints); + } + + activateProperty(key, state) { + this._panelService.property_activate(key, state); + } + + getEngineDesc(id) { + if (!this._ready || !this._engines.has(id)) + return null; + + return this._engines.get(id); + } + + async _setEngine(id, callback) { + // Send id even if id == this._currentEngineName + // because 'properties-registered' signal can be emitted + // while this._ibusSources == null on a lock screen. + if (!this._ready) { + if (callback) + callback(); + return; + } + + try { + await this._ibus.set_global_engine_async(id, + this._MAX_INPUT_SOURCE_ACTIVATION_TIME, + this._cancellable); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } + + if (callback) + callback(); + } + + async setEngine(id, callback) { + if (this._preOskState) + this._preOskState.engine = id; + + const isXkb = id.startsWith('xkb:'); + if (this._oskCompletion && isXkb) + return; + + if (this._oskCompletion) + this.setCompletionEnabled(false, callback); + else + await this._setEngine(id, callback); + } + + preloadEngines(ids) { + if (!this._ibus || !this._ready) + return; + + if (!ids.includes(TYPING_BOOSTER_ENGINE)) + ids.push(TYPING_BOOSTER_ENGINE); + + if (this._preloadEnginesId != 0) { + GLib.source_remove(this._preloadEnginesId); + this._preloadEnginesId = 0; + } + + this._preloadEnginesId = + GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, + this._PRELOAD_ENGINES_DELAY_TIME, + () => { + this._ibus.preload_engines_async( + ids, + -1, + this._cancellable, + null); + this._preloadEnginesId = 0; + return GLib.SOURCE_REMOVE; + }); + } + + setCompletionEnabled(enabled, callback) { + /* Needs typing-booster available */ + if (enabled && !this._engines.has(TYPING_BOOSTER_ENGINE)) + return false; + /* Can do only on xkb engines */ + if (enabled && !this._currentEngineName.startsWith('xkb:')) + return false; + + if (this._oskCompletion === enabled) + return true; + + this._oskCompletion = enabled; + let settings = + new Gio.Settings({schema_id: IBUS_TYPING_BOOSTER_SCHEMA}); + + if (enabled) { + this._preOskState = { + 'engine': this._currentEngineName, + 'emoji': settings.get_value(KEY_EMOJIPREDICTIONS), + 'langs': settings.get_value(KEY_DICTIONARY), + 'completion': settings.get_value(KEY_INLINECOMPLETION), + 'inputMethod': settings.get_value(KEY_INPUTMETHOD), + }; + settings.reset(KEY_EMOJIPREDICTIONS); + + const removeEncoding = l => l.replace(/\..*/, ''); + const removeDups = (l, pos, arr) => { + return !pos || arr[pos - 1] !== l; + }; + settings.set_string( + KEY_DICTIONARY, + GLib.get_language_names().map(removeEncoding) + .sort().filter(removeDups).join(',')); + + settings.reset(KEY_INLINECOMPLETION); + settings.set_string(KEY_INPUTMETHOD, 'NoIME'); + this._setEngine(TYPING_BOOSTER_ENGINE, callback); + } else if (this._preOskState) { + const {engine, emoji, langs, completion, inputMethod} = + this._preOskState; + this._preOskState = null; + this._setEngine(engine, callback); + settings.set_value(KEY_EMOJIPREDICTIONS, emoji); + settings.set_value(KEY_DICTIONARY, langs); + settings.set_value(KEY_INLINECOMPLETION, completion); + settings.set_value(KEY_INPUTMETHOD, inputMethod); + } + return true; + } +}; |