Skip to content

Commit 2822bbf

Browse files
committed
input methods: Add minimal support for fcitx5.
- Don't launch ibus if it's not the session-preferred engine - Add fcitx5 implementation of ClutterInputMethod, allowing integration with clutter entries. - fcitx provides all popups and indicators and shortcut handling. This is just a test of standalone fcitx5 support. We probably shouldn't actively break a user's preferred engine, and there are gaps in ibus' language support. There are no real user-facing changes (like to cinnamon's keyboard settings) here yet, only behavioral. Ref: #12851, #11115, #13387, #13622 Also: linuxmint/mintlocale#85, linuxmint/mintlocale#89, linuxmint/mintlocale#93
1 parent aae46bf commit 2822bbf

4 files changed

Lines changed: 349 additions & 1 deletion

File tree

js/misc/fcitxInputMethod.js

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
2+
/* exported FcitxInputMethod */
3+
4+
// Clutter.InputMethod backend that feeds Cinnamon's own Clutter actors (menu
5+
// search, run dialog, Looking Glass, ...) through fcitx5 instead of ibus, used
6+
// when fcitx is the active framework on X11 (see misc/imFramework.js).
7+
//
8+
// This mirrors misc/inputMethod.js (the ibus backend) one-to-one, but talks to
9+
// fcitx's dbusfrontend over plain Gio DBus (there is no fcitx gobject-
10+
// introspection binding the way there is for IBus):
11+
//
12+
// service org.fcitx.Fcitx5
13+
// InputMethod1 /org/freedesktop/portal/inputmethod org.fcitx.Fcitx.InputMethod1
14+
// InputContext1 (path returned by CreateInputContext) org.fcitx.Fcitx.InputContext1
15+
//
16+
// fcitx draws its own candidate popup; we only bridge input and report the
17+
// caret rectangle so that popup lands in the right place. We deliberately do
18+
// NOT request ClientSideInputPanel, so candidates are never drawn in-process.
19+
20+
const { Clutter, Gio, GLib, GObject } = imports.gi;
21+
22+
const KeyboardManager = imports.ui.keyboardManager;
23+
24+
var HIDE_PANEL_TIME = 50;
25+
26+
const FCITX_SERVICE = 'org.fcitx.Fcitx5';
27+
const FCITX_IM_PATH = '/org/freedesktop/portal/inputmethod';
28+
const FCITX_IM_IFACE = 'org.fcitx.Fcitx.InputMethod1';
29+
const FCITX_IC_IFACE = 'org.fcitx.Fcitx.InputContext1';
30+
31+
// fcitx CapabilityFlag bits (src/lib/fcitx-utils/capabilityflags.h)
32+
const CAP_PREEDIT = 1 << 1;
33+
const CAP_FORMATTED_PREEDIT = 1 << 4;
34+
const CAP_SURROUNDING_TEXT = 1 << 6;
35+
36+
var FcitxInputMethod = GObject.registerClass(
37+
class FcitxInputMethod extends Clutter.InputMethod {
38+
_init() {
39+
super._init();
40+
this._hints = 0;
41+
this._purpose = 0;
42+
this._currentFocus = null;
43+
this._preeditStr = '';
44+
this._preeditPos = 0;
45+
this._preeditVisible = false;
46+
this._hidePanelId = 0;
47+
48+
this._connection = null;
49+
this._icPath = null;
50+
this._signalIds = [];
51+
this._cancellable = new Gio.Cancellable();
52+
53+
this.connect('notify::can-show-preedit', this._updateCapabilities.bind(this));
54+
55+
this._inputSourceManager = KeyboardManager.getInputSourceManager();
56+
this._inputSourceManager.reload();
57+
this._sourceChangedId = this._inputSourceManager.connect('current-source-changed',
58+
this._onSourceChanged.bind(this));
59+
this._currentSource = this._inputSourceManager.currentSource;
60+
this._currentSource.activate(true);
61+
62+
this._watchId = Gio.bus_watch_name(Gio.BusType.SESSION, FCITX_SERVICE,
63+
Gio.BusNameWatcherFlags.NONE,
64+
this._onNameAppeared.bind(this),
65+
this._onNameVanished.bind(this));
66+
}
67+
68+
get currentFocus() {
69+
return this._currentFocus;
70+
}
71+
72+
_onSourceChanged() {
73+
this._currentSource = this._inputSourceManager.currentSource;
74+
}
75+
76+
_onNameAppeared(connection, _name, _owner) {
77+
this._connection = connection;
78+
this._createContext();
79+
}
80+
81+
_onNameVanished() {
82+
this._clear();
83+
}
84+
85+
_createContext() {
86+
// CreateInputContext(a(ss)) -> (o path, ay uuid)
87+
let args = new GLib.Variant('(a(ss))', [[['program', 'cinnamon']]]);
88+
this._connection.call(FCITX_SERVICE, FCITX_IM_PATH, FCITX_IM_IFACE,
89+
'CreateInputContext', args,
90+
new GLib.VariantType('(oay)'),
91+
Gio.DBusCallFlags.NONE, -1, this._cancellable,
92+
this._onContextCreated.bind(this));
93+
}
94+
95+
_onContextCreated(connection, res) {
96+
let path;
97+
try {
98+
[path] = connection.call_finish(res).deepUnpack();
99+
} catch (e) {
100+
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
101+
logError(e, 'fcitx: CreateInputContext failed');
102+
return;
103+
}
104+
105+
this._icPath = path;
106+
107+
let subscribe = (signal, callback) => {
108+
return this._connection.signal_subscribe(FCITX_SERVICE, FCITX_IC_IFACE,
109+
signal, this._icPath, null,
110+
Gio.DBusSignalFlags.NONE, callback);
111+
};
112+
this._signalIds.push(subscribe('CommitString', this._onCommitString.bind(this)));
113+
this._signalIds.push(subscribe('UpdateFormattedPreedit', this._onUpdatePreedit.bind(this)));
114+
this._signalIds.push(subscribe('ForwardKey', this._onForwardKey.bind(this)));
115+
this._signalIds.push(subscribe('DeleteSurroundingText', this._onDeleteSurrounding.bind(this)));
116+
117+
this._updateCapabilities();
118+
119+
if (this._currentFocus)
120+
this._icCall('FocusIn', null);
121+
}
122+
123+
_icCall(method, params, replyType = null, callback = null) {
124+
if (!this._connection || !this._icPath)
125+
return;
126+
this._connection.call(FCITX_SERVICE, this._icPath, FCITX_IC_IFACE, method,
127+
params, replyType, Gio.DBusCallFlags.NONE, -1,
128+
this._cancellable, callback);
129+
}
130+
131+
_updateCapabilities() {
132+
let caps = CAP_PREEDIT | CAP_FORMATTED_PREEDIT | CAP_SURROUNDING_TEXT;
133+
// SetCapability(t) — note: we never set ClientSideInputPanel, so fcitx
134+
// keeps drawing its own candidate popup.
135+
this._icCall('SetCapability', new GLib.Variant('(t)', [caps]));
136+
}
137+
138+
_clear() {
139+
if (this._connection && this._signalIds.length > 0) {
140+
for (let id of this._signalIds)
141+
this._connection.signal_unsubscribe(id);
142+
}
143+
this._signalIds = [];
144+
145+
if (this._connection && this._icPath)
146+
this._icCall('DestroyIC', null);
147+
148+
this._icPath = null;
149+
this._connection = null;
150+
this._hints = 0;
151+
this._purpose = 0;
152+
this._preeditStr = '';
153+
this._preeditPos = 0;
154+
this._preeditVisible = false;
155+
}
156+
157+
_onCommitString(_conn, _sender, _path, _iface, _signal, params) {
158+
let [text] = params.deepUnpack();
159+
if (text)
160+
this.commit(text);
161+
}
162+
163+
_onUpdatePreedit(_conn, _sender, _path, _iface, _signal, params) {
164+
// UpdateFormattedPreedit(a(si) strings, i cursor)
165+
let [strings, pos] = params.deepUnpack();
166+
let preedit = strings.map(s => s[0]).join('');
167+
168+
if (preedit.length > 0)
169+
this.set_preedit_text(preedit, pos);
170+
else if (this._preeditVisible)
171+
this.set_preedit_text(null, pos);
172+
173+
this._preeditStr = preedit;
174+
this._preeditPos = pos;
175+
this._preeditVisible = preedit.length > 0;
176+
}
177+
178+
_onForwardKey(_conn, _sender, _path, _iface, _signal, params) {
179+
// ForwardKey(u keyval, u state, b isRelease)
180+
let [keyval, state, isRelease] = params.deepUnpack();
181+
let press = !isRelease;
182+
183+
let curEvent = Clutter.get_current_event();
184+
let time;
185+
if (curEvent)
186+
time = curEvent.get_time();
187+
else
188+
time = global.display.get_current_time_roundtrip();
189+
190+
// fcitx doesn't give us a keycode on forwarded keys; 0 is acceptable.
191+
this.forward_key(keyval, 0, state & Clutter.ModifierType.MODIFIER_MASK, time, press);
192+
}
193+
194+
_onDeleteSurrounding(_conn, _sender, _path, _iface, _signal, params) {
195+
// DeleteSurroundingText(i offset, u nchars)
196+
let [offset, nchars] = params.deepUnpack();
197+
try {
198+
this.delete_surrounding(offset, nchars);
199+
} catch (e) {
200+
this.delete_surrounding(0, nchars + offset);
201+
}
202+
}
203+
204+
vfunc_focus_in(focus) {
205+
this._currentFocus = focus;
206+
this._icCall('FocusIn', null);
207+
208+
if (this._hidePanelId) {
209+
GLib.source_remove(this._hidePanelId);
210+
this._hidePanelId = 0;
211+
}
212+
}
213+
214+
vfunc_focus_out() {
215+
this._currentFocus = null;
216+
this._icCall('FocusOut', null);
217+
218+
if (this._preeditStr) {
219+
this.set_preedit_text(null, 0);
220+
this._preeditStr = null;
221+
}
222+
223+
this._hidePanelId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, HIDE_PANEL_TIME, () => {
224+
this.set_input_panel_state(Clutter.InputPanelState.OFF);
225+
this._hidePanelId = 0;
226+
return GLib.SOURCE_REMOVE;
227+
});
228+
}
229+
230+
vfunc_reset() {
231+
this._icCall('Reset', null);
232+
233+
if (this._preeditStr) {
234+
this.set_preedit_text(null, 0);
235+
this._preeditStr = null;
236+
}
237+
}
238+
239+
vfunc_set_cursor_location(rect) {
240+
// fcitx positions its own candidate popup from this. On X11 the stage
241+
// covers the screen, so stage coords double as screen coords.
242+
this._icCall('SetCursorRect',
243+
new GLib.Variant('(iiii)', [rect.get_x(), rect.get_y(),
244+
rect.get_width(), rect.get_height()]));
245+
}
246+
247+
vfunc_set_surrounding(text, cursor, anchor) {
248+
if (!text)
249+
return;
250+
// SetSurroundingText(s text, u cursor, u anchor)
251+
this._icCall('SetSurroundingText',
252+
new GLib.Variant('(suu)', [text, cursor, anchor]));
253+
}
254+
255+
vfunc_update_content_hints(hints) {
256+
// fcitx expresses hints/purpose through capability flags differently from
257+
// ibus; the base capabilities are sufficient for v1. Track for parity.
258+
this._hints = hints;
259+
}
260+
261+
vfunc_update_content_purpose(purpose) {
262+
this._purpose = purpose;
263+
}
264+
265+
vfunc_filter_key_event(event) {
266+
if (!this._connection || !this._icPath)
267+
return false;
268+
if (!this._currentSource)
269+
return false;
270+
271+
let isRelease = event.type() == Clutter.EventType.KEY_RELEASE;
272+
let keyval = event.get_key_symbol();
273+
// fcitx's own GTK frontend passes the X hardware keycode (evdev + 8),
274+
// which is exactly what Clutter.get_key_code() returns — so no -8 here.
275+
let keycode = event.get_key_code();
276+
let state = event.get_state() & Clutter.ModifierType.MODIFIER_MASK;
277+
let time = event.get_time();
278+
279+
// ProcessKeyEvent(u keyval, u keycode, u state, b isRelease, u time) -> (b handled)
280+
this._connection.call(FCITX_SERVICE, this._icPath, FCITX_IC_IFACE,
281+
'ProcessKeyEvent',
282+
new GLib.Variant('(uuubu)', [keyval, keycode, state, isRelease, time]),
283+
new GLib.VariantType('(b)'),
284+
Gio.DBusCallFlags.NONE, -1, this._cancellable,
285+
(connection, res) => {
286+
let handled = false;
287+
try {
288+
[handled] = connection.call_finish(res).deepUnpack();
289+
} catch (e) {
290+
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
291+
return;
292+
log(`Error processing key on fcitx: ${e.message}`);
293+
}
294+
this.notify_key_event(event, handled);
295+
});
296+
return true;
297+
}
298+
});

js/misc/ibusManager.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { Gio, GLib, IBus, Meta } = imports.gi;
55
const Signals = imports.signals;
66

77
const IBusCandidatePopup = imports.ui.ibusCandidatePopup;
8+
const IMFramework = imports.misc.imFramework;
89

910
// Ensure runtime version matches
1011
_checkIBusVersion(1, 5, 2);
@@ -47,6 +48,12 @@ var IBusManager = class {
4748
this._registerPropertiesId = 0;
4849
this._currentEngineName = null;
4950
this._preloadEnginesId = 0;
51+
this._ibus = null;
52+
53+
if (IMFramework.getFramework() !== IMFramework.FRAMEWORK_IBUS) {
54+
// Another framework (e.g. fcitx) owns input on this session.
55+
return;
56+
}
5057

5158
this._ibus = IBus.Bus.new_async();
5259
this._ibus.connect('connected', this._onConnected.bind(this));

js/misc/imFramework.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
2+
/* exported getFramework, FRAMEWORK_IBUS, FRAMEWORK_FCITX, FRAMEWORK_NONE */
3+
4+
// Which input-method framework is active for this session.
5+
//
6+
// On X11 the choice is made by the user via mintlocale-im / im-config, which
7+
// exports GTK_IM_MODULE / XMODIFIERS at session start. We trust those: they are
8+
// the live truth for this session and need no subprocess to query.
9+
//
10+
// On Wayland, muffin routes app text-input (text-input-v3) through the in-process
11+
// ClutterInputMethod, which only has an ibus backend; there is no path for an
12+
// external fcitx to connect. So Wayland is always treated as ibus for now.
13+
//
14+
// Only an explicit fcitx selection changes behaviour; everything else stays on
15+
// the historical ibus path, so non-fcitx sessions are unaffected.
16+
17+
const { GLib, Meta } = imports.gi;
18+
19+
var FRAMEWORK_IBUS = 'ibus';
20+
var FRAMEWORK_FCITX = 'fcitx';
21+
var FRAMEWORK_NONE = 'none';
22+
23+
let _framework = null;
24+
25+
function getFramework() {
26+
if (_framework != null)
27+
return _framework;
28+
29+
if (Meta.is_wayland_compositor()) {
30+
_framework = FRAMEWORK_IBUS;
31+
return _framework;
32+
}
33+
34+
let mod = (GLib.getenv('GTK_IM_MODULE') || GLib.getenv('XMODIFIERS') || '').toLowerCase();
35+
_framework = mod.includes('fcitx') ? FRAMEWORK_FCITX : FRAMEWORK_IBUS;
36+
37+
return _framework;
38+
}

js/ui/main.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ const Systray = imports.ui.systray;
131131
const Accessibility = imports.ui.accessibility;
132132
const ModalDialog = imports.ui.modalDialog;
133133
const InputMethod = imports.misc.inputMethod;
134+
const FcitxInputMethod = imports.misc.fcitxInputMethod;
135+
const IMFramework = imports.misc.imFramework;
134136
const ScreenRecorder = imports.ui.screenRecorder;
135137
const {GesturesManager} = imports.ui.gestures.gesturesManager;
136138
const {MonitorLabeler} = imports.ui.monitorLabeler;
@@ -524,7 +526,10 @@ function start() {
524526

525527
_loadOskLayouts();
526528
keyboardManager = new KeyboardManager();
527-
inputMethod = new InputMethod.InputMethod();
529+
if (IMFramework.getFramework() === IMFramework.FRAMEWORK_FCITX)
530+
inputMethod = new FcitxInputMethod.FcitxInputMethod();
531+
else
532+
inputMethod = new InputMethod.InputMethod();
528533
Clutter.get_default_backend().set_input_method(inputMethod);
529534
virtualKeyboardManager = new VirtualKeyboard.VirtualKeyboardManager();
530535
virtualKeyboardManager.connect("enabled-changed", () => {

0 commit comments

Comments
 (0)