gnome-shell/js/misc/inputMethod.js
Carlos Garnacho 76090c14eb inputMethod: Check return value when letting IM handle virtual keys
When propagating keys from the OSK, we usually feed these directly to
the IBusInputContext and let the IM handle the effects of this virtual
key event (which may also include feeding a key event back to us).

But these functions may also return a FALSE value if the key was "let
through" by the IM, which means the ball is in our yard again, and
we are responsible of letting this event get to its destination.

If that happens, just fall through, so the string is committed to
the client as an UTF-8 string, or propagated through keyboard events.

Closes: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/5930
Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2508>
2022-10-18 19:31:51 +00:00

351 lines
12 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported InputMethod */
const { Clutter, GLib, Gio, GObject, IBus } = imports.gi;
const Keyboard = imports.ui.status.keyboard;
const Main = imports.ui.main;
Gio._promisify(IBus.Bus.prototype,
'create_input_context_async', 'create_input_context_async_finish');
Gio._promisify(IBus.InputContext.prototype,
'process_key_event_async', 'process_key_event_async_finish');
var HIDE_PANEL_TIME = 50;
var InputMethod = GObject.registerClass({
Signals: {
'surrounding-text-set': {},
'terminal-mode-changed': {},
},
}, class InputMethod extends Clutter.InputMethod {
_init() {
super._init();
this._hints = 0;
this._purpose = 0;
this._currentFocus = null;
this._preeditStr = '';
this._preeditPos = 0;
this._preeditAnchor = 0;
this._preeditVisible = false;
this._hidePanelId = 0;
this.terminalMode = false;
this._ibus = IBus.Bus.new_async();
this._ibus.connect('connected', this._onConnected.bind(this));
this._ibus.connect('disconnected', this._clear.bind(this));
this.connect('notify::can-show-preedit', this._updateCapabilities.bind(this));
this._inputSourceManager = Keyboard.getInputSourceManager();
this._sourceChangedId = this._inputSourceManager.connect('current-source-changed',
this._onSourceChanged.bind(this));
this._currentSource = this._inputSourceManager.currentSource;
if (this._ibus.is_connected())
this._onConnected();
}
get currentFocus() {
return this._currentFocus;
}
_updateCapabilities() {
let caps = IBus.Capabilite.PREEDIT_TEXT | IBus.Capabilite.FOCUS | IBus.Capabilite.SURROUNDING_TEXT;
if (Main.keyboard.visible)
caps |= IBus.Capabilite.OSK;
if (this._context)
this._context.set_capabilities(caps);
}
_onSourceChanged() {
this._currentSource = this._inputSourceManager.currentSource;
}
async _onConnected() {
this._cancellable = new Gio.Cancellable();
try {
this._context = await this._ibus.create_input_context_async(
'gnome-shell', -1, this._cancellable);
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
logError(e);
this._clear();
}
return;
}
this._context.set_client_commit_preedit(true);
this._context.connect('commit-text', this._onCommitText.bind(this));
this._context.connect('delete-surrounding-text', this._onDeleteSurroundingText.bind(this));
this._context.connect('update-preedit-text-with-mode', this._onUpdatePreeditText.bind(this));
this._context.connect('show-preedit-text', this._onShowPreeditText.bind(this));
this._context.connect('hide-preedit-text', this._onHidePreeditText.bind(this));
this._context.connect('forward-key-event', this._onForwardKeyEvent.bind(this));
this._context.connect('destroy', this._clear.bind(this));
Main.keyboard.connectObject('visibility-changed', () => this._updateCapabilities());
this._updateCapabilities();
}
_clear() {
Main.keyboard.disconnectObject(this);
if (this._cancellable) {
this._cancellable.cancel();
this._cancellable = null;
}
this._context = null;
this._hints = 0;
this._purpose = 0;
this._preeditStr = '';
this._preeditPos = 0;
this._preeditAnchor = 0;
this._preeditVisible = false;
}
_emitRequestSurrounding() {
if (this._context.needs_surrounding_text())
this.emit('request-surrounding');
}
_onCommitText(_context, text) {
this.commit(text.get_text());
}
_onDeleteSurroundingText(_context, offset, nchars) {
try {
this.delete_surrounding(offset, nchars);
} catch (e) {
// We may get out of bounds for negative offset on older mutter
this.delete_surrounding(0, nchars + offset);
}
}
_onUpdatePreeditText(_context, text, pos, visible, mode) {
if (text == null)
return;
let preedit = text.get_text();
if (preedit === '')
preedit = null;
const anchor = pos;
if (visible)
this.set_preedit_text(preedit, pos, anchor, mode);
else if (this._preeditVisible)
this.set_preedit_text(null, pos, anchor, mode);
this._preeditStr = preedit;
this._preeditPos = pos;
this._preeditAnchor = anchor;
this._preeditVisible = visible;
this._preeditCommitMode = mode;
}
_onShowPreeditText() {
this._preeditVisible = true;
this.set_preedit_text(
this._preeditStr, this._preeditPos, this._preeditAnchor,
this._preeditCommitMode);
}
_onHidePreeditText() {
this.set_preedit_text(
null, this._preeditPos, this._preeditAnchor,
this._preeditCommitMode);
this._preeditVisible = false;
}
_onForwardKeyEvent(_context, keyval, keycode, state) {
let press = (state & IBus.ModifierType.RELEASE_MASK) == 0;
state &= ~IBus.ModifierType.RELEASE_MASK;
let curEvent = Clutter.get_current_event();
let time;
if (curEvent)
time = curEvent.get_time();
else
time = global.display.get_current_time_roundtrip();
this.forward_key(keyval, keycode + 8, state & Clutter.ModifierType.MODIFIER_MASK, time, press);
}
vfunc_focus_in(focus) {
this._currentFocus = focus;
if (this._context) {
this._context.focus_in();
this._emitRequestSurrounding();
}
if (this._hidePanelId) {
GLib.source_remove(this._hidePanelId);
this._hidePanelId = 0;
}
}
vfunc_focus_out() {
this._currentFocus = null;
if (this._context)
this._context.focus_out();
if (this._preeditStr && this._preeditVisible) {
// Unset any preedit text
this.set_preedit_text(null, 0, 0, this._preeditCommitMode);
this._preeditStr = null;
}
this._hidePanelId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, HIDE_PANEL_TIME, () => {
this.set_input_panel_state(Clutter.InputPanelState.OFF);
this._hidePanelId = 0;
return GLib.SOURCE_REMOVE;
});
}
vfunc_reset() {
if (this._context) {
this._context.reset();
this._emitRequestSurrounding();
}
this._surroundingText = null;
this._surroundingTextCursor = null;
this._preeditStr = null;
this._setTerminalMode(false);
}
vfunc_set_cursor_location(rect) {
if (this._context) {
this._context.set_cursor_location(rect.get_x(), rect.get_y(),
rect.get_width(), rect.get_height());
this._emitRequestSurrounding();
}
}
vfunc_set_surrounding(text, cursor, anchor) {
this._surroundingText = text;
this._surroundingTextCursor = cursor;
this.emit('surrounding-text-set');
if (!this._context || !text)
return;
let ibusText = IBus.Text.new_from_string(text);
this._context.set_surrounding_text(ibusText, cursor, anchor);
}
vfunc_update_content_hints(hints) {
let ibusHints = 0;
if (hints & Clutter.InputContentHintFlags.COMPLETION)
ibusHints |= IBus.InputHints.WORD_COMPLETION;
if (hints & Clutter.InputContentHintFlags.SPELLCHECK)
ibusHints |= IBus.InputHints.SPELLCHECK;
if (hints & Clutter.InputContentHintFlags.AUTO_CAPITALIZATION)
ibusHints |= IBus.InputHints.UPPERCASE_SENTENCES;
if (hints & Clutter.InputContentHintFlags.LOWERCASE)
ibusHints |= IBus.InputHints.LOWERCASE;
if (hints & Clutter.InputContentHintFlags.UPPERCASE)
ibusHints |= IBus.InputHints.UPPERCASE_CHARS;
if (hints & Clutter.InputContentHintFlags.TITLECASE)
ibusHints |= IBus.InputHints.UPPERCASE_WORDS;
this._hints = ibusHints;
if (this._context)
this._context.set_content_type(this._purpose, this._hints);
}
vfunc_update_content_purpose(purpose) {
let ibusPurpose = 0;
if (purpose == Clutter.InputContentPurpose.NORMAL)
ibusPurpose = IBus.InputPurpose.FREE_FORM;
else if (purpose == Clutter.InputContentPurpose.ALPHA)
ibusPurpose = IBus.InputPurpose.ALPHA;
else if (purpose == Clutter.InputContentPurpose.DIGITS)
ibusPurpose = IBus.InputPurpose.DIGITS;
else if (purpose == Clutter.InputContentPurpose.NUMBER)
ibusPurpose = IBus.InputPurpose.NUMBER;
else if (purpose == Clutter.InputContentPurpose.PHONE)
ibusPurpose = IBus.InputPurpose.PHONE;
else if (purpose == Clutter.InputContentPurpose.URL)
ibusPurpose = IBus.InputPurpose.URL;
else if (purpose == Clutter.InputContentPurpose.EMAIL)
ibusPurpose = IBus.InputPurpose.EMAIL;
else if (purpose == Clutter.InputContentPurpose.NAME)
ibusPurpose = IBus.InputPurpose.NAME;
else if (purpose == Clutter.InputContentPurpose.PASSWORD)
ibusPurpose = IBus.InputPurpose.PASSWORD;
else if (purpose === Clutter.InputContentPurpose.TERMINAL &&
IBus.InputPurpose.TERMINAL)
ibusPurpose = IBus.InputPurpose.TERMINAL;
this._setTerminalMode(
purpose === Clutter.InputContentPurpose.TERMINAL);
this._purpose = ibusPurpose;
if (this._context)
this._context.set_content_type(this._purpose, this._hints);
}
_setTerminalMode(terminalMode) {
if (this.terminalMode !== terminalMode) {
this.terminalMode = terminalMode;
this.emit('terminal-mode-changed');
}
}
vfunc_filter_key_event(event) {
if (!this._context)
return false;
if (!this._currentSource)
return false;
let state = event.get_state();
if (state & IBus.ModifierType.IGNORED_MASK)
return false;
if (event.type() == Clutter.EventType.KEY_RELEASE)
state |= IBus.ModifierType.RELEASE_MASK;
this._context.process_key_event_async(
event.get_key_symbol(),
event.get_key_code() - 8, // Convert XKB keycodes to evcodes
state, -1, this._cancellable,
(context, res) => {
if (context != this._context)
return;
try {
let retval = context.process_key_event_async_finish(res);
this.notify_key_event(event, retval);
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
log(`Error processing key on IM: ${e.message}`);
}
});
return true;
}
getSurroundingText() {
return [this._surroundingText, this._surroundingTextCursor];
}
hasPreedit() {
return this._preeditVisible && this._preeditStr !== '' && this._preeditStr !== null;
}
async handleVirtualKey(keyval) {
try {
if (!await this._context.process_key_event_async(
keyval, 0, 0, -1, null))
return false;
await this._context.process_key_event_async(
keyval, 0, IBus.ModifierType.RELEASE_MASK, -1, null);
return true;
} catch (e) {
return false;
}
}
});