summaryrefslogtreecommitdiffstats
path: root/js/misc/ibusManager.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/misc/ibusManager.js')
-rw-r--r--js/misc/ibusManager.js398
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;
+ }
+};