Skip to content

Commit 8a446aa

Browse files
committed
Implement the InputMethod
1 parent 4b15f84 commit 8a446aa

4 files changed

Lines changed: 318 additions & 1 deletion

File tree

meson.build

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ gio_unix_dep = dependency('gio-unix-2.0', version: '>= @0@'.format(glib_version_
7070
gmodule_dep = dependency('gmodule-2.0')
7171
gee_dep = dependency('gee-0.8')
7272
gnome_desktop_dep = dependency('gnome-desktop-4')
73+
ibus_dep = dependency('ibus-1.0')
7374
gnome_bg_dep = dependency('gnome-bg-4')
7475
m_dep = cc.find_library('m', required: false)
7576
posix_dep = vala.find_library('posix', required: false)
@@ -171,7 +172,7 @@ endif
171172
add_project_arguments(vala_flags, language: 'vala')
172173
add_project_link_arguments(['-Wl,-rpath,@0@'.format(mutter_typelib_dir)], language: 'c')
173174

174-
gala_base_dep = [atk_bridge_dep, gdk_pixbuf_def, gtk4_dep, glib_dep, gobject_dep, gio_dep, gio_unix_dep, gmodule_dep, gee_dep, mutter_dep, gnome_desktop_dep, gnome_bg_dep, m_dep, posix_dep, sqlite3_dep, xext_dep]
175+
gala_base_dep = [atk_bridge_dep, gdk_pixbuf_def, gtk4_dep, glib_dep, gobject_dep, gio_dep, gio_unix_dep, gmodule_dep, gee_dep, mutter_dep, gnome_desktop_dep, gnome_bg_dep, ibus_dep, m_dep, posix_dep, sqlite3_dep, xext_dep]
175176

176177
if get_option('systemd')
177178
gala_base_dep += systemd_dep

src/InputMethod.vala

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/*
2+
* Copyright 2026 elementary, Inc. (https://elementary.io)
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*
5+
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
6+
*/
7+
8+
public class Gala.InputMethod : Clutter.InputMethod {
9+
public Meta.Display display { private get; construct; }
10+
public Graphene.Rect cursor_location { get; private set; }
11+
12+
private IBus.Bus bus;
13+
private IBus.InputContext context;
14+
15+
private bool preedit_visible;
16+
private string? preedit_text;
17+
private uint preedit_cursor;
18+
private uint preedit_anchor;
19+
private Clutter.PreeditResetMode preedit_mode;
20+
21+
private string? surrounding_text;
22+
private uint surrounding_cursor;
23+
private uint surrounding_anchor;
24+
25+
private IBus.InputPurpose input_purpose;
26+
private IBus.InputHints input_hints;
27+
28+
public InputMethod (Meta.Display display) {
29+
Object (display: display);
30+
}
31+
32+
construct {
33+
IBus.init ();
34+
35+
bus = new IBus.Bus.async ();
36+
bus.connected.connect (on_connected);
37+
38+
if (bus.is_connected ()) {
39+
on_connected ();
40+
}
41+
}
42+
43+
private void on_connected () {
44+
bus.create_input_context_async.begin ("gala", -1, null, on_input_context_created);
45+
}
46+
47+
private void on_input_context_created (Object? obj, AsyncResult res) {
48+
try {
49+
context = bus.create_input_context_async_finish (res);
50+
} catch (Error e) {
51+
warning ("Failed to create IBus input context: %s", e.message);
52+
return;
53+
}
54+
55+
context.commit_text.connect (on_commit_text);
56+
context.require_surrounding_text.connect (on_require_surrounding_text);
57+
context.delete_surrounding_text.connect (on_delete_surrounding_text);
58+
context.update_preedit_text.connect (on_update_preedit_text);
59+
context.update_preedit_text_with_mode.connect (on_update_preedit_text_with_mode);
60+
context.show_preedit_text.connect (on_show_preedit_text);
61+
context.hide_preedit_text.connect (on_hide_preedit_text);
62+
context.forward_key_event.connect (on_forward_key_event);
63+
context.destroy.connect (on_destroy);
64+
65+
update_capabilities ();
66+
}
67+
68+
private void update_capabilities () {
69+
IBus.Capabilite caps = PREEDIT_TEXT | FOCUS | SURROUNDING_TEXT;
70+
71+
// if (surrounding_text != null) {
72+
// caps |= SURROUNDING_TEXT;
73+
// }
74+
75+
// TODO: Update with sourrounding text and osk
76+
77+
context.set_capabilities (caps);
78+
}
79+
80+
private void on_commit_text (IBus.Text text) {
81+
commit (text.text);
82+
}
83+
84+
private void on_require_surrounding_text () {
85+
request_surrounding ();
86+
}
87+
88+
private void on_delete_surrounding_text (int offset, uint length) {
89+
delete_surrounding (offset, length);
90+
}
91+
92+
private void on_update_preedit_text (IBus.Text text, uint cursor_pos, bool visible) {
93+
on_update_preedit_text_with_mode (text, cursor_pos, visible, preedit_mode);
94+
}
95+
96+
private void on_update_preedit_text_with_mode (IBus.Text text, uint cursor_pos, bool visible, uint mode) {
97+
var preedit = text.text;
98+
99+
if (preedit == "") {
100+
preedit = null;
101+
}
102+
103+
var anchor = cursor_pos;
104+
105+
if (visible) {
106+
set_preedit_text (preedit, cursor_pos, anchor, mode);
107+
} else if (preedit_visible) {
108+
set_preedit_text (null, cursor_pos, anchor, mode);
109+
}
110+
111+
preedit_visible = visible;
112+
preedit_text = preedit;
113+
preedit_cursor = cursor_pos;
114+
preedit_anchor = anchor;
115+
preedit_mode = (Clutter.PreeditResetMode) mode;
116+
}
117+
118+
private void on_show_preedit_text () {
119+
preedit_visible = true;
120+
set_preedit_text (preedit_text, preedit_cursor, preedit_anchor, preedit_mode);
121+
}
122+
123+
private void on_hide_preedit_text () {
124+
set_preedit_text (null, preedit_cursor, preedit_anchor, preedit_mode);
125+
preedit_visible = false;
126+
}
127+
128+
private void on_forward_key_event (uint keyval, uint keycode, uint _modifiers) {
129+
var modifiers = (IBus.ModifierType) _modifiers;
130+
var press = !(IBus.ModifierType.RELEASE_MASK in modifiers);
131+
modifiers &= ~IBus.ModifierType.RELEASE_MASK;
132+
133+
var time = display.get_current_time ();
134+
135+
forward_key (keyval, keycode + 8, modifiers & Clutter.ModifierType.MODIFIER_MASK, time, press);
136+
}
137+
138+
private void on_destroy () {
139+
// TODO: Handle?
140+
critical ("IBus input context was destroyed");
141+
context = null;
142+
}
143+
144+
public override void focus_in (Clutter.InputFocus actor) {
145+
update_capabilities ();
146+
context.set_content_type (input_purpose, input_hints);
147+
request_surrounding ();
148+
149+
context.focus_in ();
150+
}
151+
152+
public override void focus_out () {
153+
context.set_content_type (0, 0);
154+
context.reset ();
155+
156+
context.focus_out ();
157+
158+
if (preedit_visible) {
159+
set_preedit_text (null, preedit_cursor, preedit_anchor, preedit_mode);
160+
preedit_text = null;
161+
}
162+
}
163+
164+
public override void reset () {
165+
context.reset ();
166+
request_surrounding ();
167+
168+
surrounding_text = null;
169+
surrounding_cursor = 0;
170+
surrounding_anchor = 0;
171+
preedit_text = null;
172+
}
173+
174+
public override void set_cursor_location (Graphene.Rect rect) {
175+
context.set_cursor_location ((int) rect.origin.x, (int) rect.origin.y, (int) rect.size.width, (int) rect.size.height);
176+
cursor_location = rect;
177+
}
178+
179+
public override void set_surrounding (string text, uint cursor_index, uint anchor_index) {
180+
var update_caps = (surrounding_text == null) != (text == null);
181+
182+
surrounding_text = text;
183+
surrounding_cursor = cursor_index;
184+
surrounding_anchor = anchor_index;
185+
186+
if (update_caps) {
187+
update_capabilities ();
188+
}
189+
190+
if (text == null) {
191+
return;
192+
}
193+
194+
var ibus_text = new IBus.Text.from_string (text);
195+
context.set_surrounding_text (ibus_text, cursor_index, anchor_index);
196+
}
197+
198+
public override bool filter_key_event (Clutter.Event event) {
199+
var state = (IBus.ModifierType) event.get_state ();
200+
201+
if (IBus.ModifierType.IGNORED_MASK in state) {
202+
return false;
203+
}
204+
205+
if (event.get_type () == Clutter.EventType.KEY_RELEASE) {
206+
state |= IBus.ModifierType.RELEASE_MASK;
207+
}
208+
209+
context.process_key_event_async.begin (
210+
event.get_key_symbol (), event.get_key_code () - 8, state, -1, null,
211+
(obj, res) => {
212+
try {
213+
var handled = context.process_key_event_async_finish (res);
214+
notify_key_event (event, handled);
215+
} catch (Error e) {
216+
warning ("Failed to process key event on IM: %s", e.message);
217+
}
218+
}
219+
);
220+
221+
return true;
222+
}
223+
224+
public override void update_content_hints (Clutter.InputContentHintFlags hints) {
225+
IBus.InputHints ibus_hints = 0;
226+
227+
if (COMPLETION in hints) {
228+
ibus_hints |= IBus.InputHints.WORD_COMPLETION;
229+
}
230+
231+
if (SPELLCHECK in hints) {
232+
ibus_hints |= IBus.InputHints.SPELLCHECK;
233+
}
234+
235+
if (AUTO_CAPITALIZATION in hints) {
236+
ibus_hints |= IBus.InputHints.UPPERCASE_SENTENCES;
237+
}
238+
239+
if (LOWERCASE in hints) {
240+
ibus_hints |= IBus.InputHints.LOWERCASE;
241+
}
242+
243+
if (UPPERCASE in hints) {
244+
ibus_hints |= IBus.InputHints.UPPERCASE_CHARS;
245+
}
246+
247+
if (TITLECASE in hints) {
248+
ibus_hints |= IBus.InputHints.UPPERCASE_WORDS;
249+
}
250+
251+
if (SENSITIVE_DATA in hints) {
252+
ibus_hints |= IBus.InputHints.PRIVATE;
253+
}
254+
255+
if (HIDDEN_TEXT in hints) {
256+
// TODO: Probably needs a newer version
257+
// ibus_hints |= IBus.InputHints.HIDDEN_TEXT;
258+
}
259+
260+
input_hints = ibus_hints;
261+
262+
context.set_content_type (input_purpose, input_hints);
263+
}
264+
265+
public override void update_content_purpose (Clutter.InputContentPurpose purpose) {
266+
IBus.InputPurpose ibus_purpose;
267+
268+
switch (purpose) {
269+
case NORMAL:
270+
ibus_purpose = FREE_FORM;
271+
break;
272+
case ALPHA:
273+
ibus_purpose = ALPHA;
274+
break;
275+
case DIGITS:
276+
ibus_purpose = DIGITS;
277+
break;
278+
case NUMBER:
279+
ibus_purpose = NUMBER;
280+
break;
281+
case PHONE:
282+
ibus_purpose = PHONE;
283+
break;
284+
case URL:
285+
ibus_purpose = URL;
286+
break;
287+
case EMAIL:
288+
ibus_purpose = EMAIL;
289+
break;
290+
case NAME:
291+
ibus_purpose = NAME;
292+
break;
293+
case PASSWORD:
294+
ibus_purpose = PASSWORD;
295+
break;
296+
case TERMINAL:
297+
ibus_purpose = TERMINAL;
298+
break;
299+
default:
300+
warning ("Unknown input purpose: %d", purpose);
301+
ibus_purpose = FREE_FORM;
302+
break;
303+
}
304+
305+
input_purpose = ibus_purpose;
306+
307+
context.set_content_type (input_purpose, input_hints);
308+
}
309+
}

src/WindowManager.vala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ namespace Gala {
8585

8686
private KeyboardManager keyboard_manager;
8787

88+
private InputMethod input_method;
89+
8890
public WindowTracker? window_tracker { get; private set; }
8991

9092
private WindowMover window_mover;
@@ -133,6 +135,9 @@ namespace Gala {
133135
}
134136

135137
public override void start () {
138+
input_method = new InputMethod (get_display ());
139+
Clutter.get_default_backend ().set_input_method (input_method);
140+
136141
ShellClientsManager.init (this);
137142
BlurManager.init (this);
138143
daemon_manager = new DaemonManager (get_display ());

src/meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ gala_bin_sources = files(
44
'DBusAccelerator.vala',
55
'DaemonManager.vala',
66
'DesktopIntegration.vala',
7+
'IBusManager.vala',
8+
'InputMethod.vala',
79
'InternalUtils.vala',
810
'KeyboardManager.vala',
911
'Main.vala',

0 commit comments

Comments
 (0)