Skip to content

Commit cb688b2

Browse files
committed
Implement the InputMethod
1 parent 85d21b4 commit cb688b2

6 files changed

Lines changed: 328 additions & 4 deletions

File tree

.github/workflows/main.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
- name: Install Dependencies
3232
run: |
3333
apt update
34-
apt install -y gettext gsettings-desktop-schemas-dev libatk-bridge2.0-dev libclutter-1.0-dev libgee-0.8-dev libglib2.0-dev libgnome-desktop-4-dev libgnome-bg-4-dev libgranite-dev libgtk-3-dev ${{ matrix.mutter_pkg }} libsoup-3.0-dev libsqlite3-dev meson systemd-dev valac valadoc
34+
apt install -y gettext gsettings-desktop-schemas-dev libatk-bridge2.0-dev libclutter-1.0-dev libgee-0.8-dev libglib2.0-dev libgnome-desktop-4-dev libgnome-bg-4-dev libgranite-dev libgtk-3-dev libibus-1.0-dev ${{ matrix.mutter_pkg }} libsoup-3.0-dev libsqlite3-dev meson systemd-dev valac valadoc
3535
- name: Build
3636
env:
3737
DESTDIR: out
@@ -53,7 +53,7 @@ jobs:
5353
- uses: actions/checkout@v6
5454
- name: Install Dependencies
5555
run: |
56-
dnf install -y desktop-file-utils gettext gsettings-desktop-schemas-devel atk-devel clutter-devel libgee-devel glib2-devel gnome-desktop3-devel granite-devel granite-7-devel gtk3-devel gtk4-devel libhandy-devel mutter-devel sqlite-devel meson valac valadoc
56+
dnf install -y desktop-file-utils gettext gsettings-desktop-schemas-devel atk-devel clutter-devel libgee-devel glib2-devel gnome-desktop3-devel granite-devel granite-7-devel gtk3-devel gtk4-devel ibus-devel libhandy-devel mutter-devel sqlite-devel meson valac valadoc
5757
- name: Build
5858
env:
5959
DESTDIR: out
@@ -72,7 +72,7 @@ jobs:
7272
run: |
7373
zypper addrepo https://download.opensuse.org/repositories/X11:Pantheon/16.0/X11:Pantheon.repo
7474
zypper --gpg-auto-import-keys refresh
75-
zypper --non-interactive install tar git desktop-file-utils gsettings-desktop-schemas-devel libatk-1_0-0 clutter-devel libgee-devel glib2-devel libgnome-desktop-4-devel granite6-devel granite-devel gtk3-devel gtk4-devel libhandy-devel mutter-devel sqlite3-devel meson vala valadoc gcc
75+
zypper --non-interactive install tar git desktop-file-utils gsettings-desktop-schemas-devel libatk-1_0-0 clutter-devel libgee-devel glib2-devel libgnome-desktop-4-devel granite6-devel granite-devel gtk3-devel gtk4-devel ibus-devel libhandy-devel mutter-devel sqlite3-devel meson vala valadoc gcc
7676
- uses: actions/checkout@v6
7777
- name: Build
7878
env:

docs/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ all_doc_target = custom_target(
4343
'--pkg', 'atk-bridge-2.0',
4444
'--pkg', 'gnome-bg-4',
4545
'--pkg', 'gnome-desktop-4',
46+
'--pkg', 'ibus-1.0',
4647
'--pkg', 'libsystemd',
4748
'--pkg', 'wayland-server',
4849
'--pkg', 'pantheon-desktop-shell',

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: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
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+
context?.set_capabilities (caps);
76+
}
77+
78+
private void on_commit_text (IBus.Text text) {
79+
commit (text.text);
80+
}
81+
82+
private void on_require_surrounding_text () {
83+
request_surrounding ();
84+
}
85+
86+
private void on_delete_surrounding_text (int offset, uint length) {
87+
delete_surrounding (offset, length);
88+
}
89+
90+
private void on_update_preedit_text (IBus.Text text, uint cursor_pos, bool visible) {
91+
on_update_preedit_text_with_mode (text, cursor_pos, visible, preedit_mode);
92+
}
93+
94+
private void on_update_preedit_text_with_mode (IBus.Text text, uint cursor_pos, bool visible, uint mode) {
95+
var preedit = text.text;
96+
97+
if (preedit == "") {
98+
preedit = null;
99+
}
100+
101+
var anchor = cursor_pos;
102+
103+
if (visible) {
104+
set_preedit_text (preedit, cursor_pos, anchor, mode);
105+
} else if (preedit_visible) {
106+
set_preedit_text (null, cursor_pos, anchor, mode);
107+
}
108+
109+
preedit_visible = visible;
110+
preedit_text = preedit;
111+
preedit_cursor = cursor_pos;
112+
preedit_anchor = anchor;
113+
preedit_mode = (Clutter.PreeditResetMode) mode;
114+
}
115+
116+
private void on_show_preedit_text () {
117+
preedit_visible = true;
118+
set_preedit_text (preedit_text, preedit_cursor, preedit_anchor, preedit_mode);
119+
}
120+
121+
private void on_hide_preedit_text () {
122+
set_preedit_text (null, preedit_cursor, preedit_anchor, preedit_mode);
123+
preedit_visible = false;
124+
}
125+
126+
private void on_forward_key_event (uint keyval, uint keycode, uint _modifiers) {
127+
var modifiers = (IBus.ModifierType) _modifiers;
128+
var press = !(IBus.ModifierType.RELEASE_MASK in modifiers);
129+
modifiers &= ~IBus.ModifierType.RELEASE_MASK;
130+
131+
var time = display.get_current_time ();
132+
133+
forward_key (keyval, keycode + 8, modifiers & Clutter.ModifierType.MODIFIER_MASK, time, press);
134+
}
135+
136+
private void on_destroy () {
137+
debug ("IBus input context was destroyed");
138+
context = null;
139+
}
140+
141+
private void maybe_request_surrounding () {
142+
if (context != null && context.needs_surrounding_text ()) {
143+
request_surrounding ();
144+
}
145+
}
146+
147+
public override void focus_in (Clutter.InputFocus actor) {
148+
update_capabilities ();
149+
context?.set_content_type (input_purpose, input_hints);
150+
maybe_request_surrounding ();
151+
152+
context?.focus_in ();
153+
}
154+
155+
public override void focus_out () {
156+
context?.set_content_type (0, 0);
157+
context?.reset ();
158+
159+
context?.focus_out ();
160+
161+
if (preedit_visible) {
162+
set_preedit_text (null, preedit_cursor, preedit_anchor, preedit_mode);
163+
preedit_text = null;
164+
}
165+
}
166+
167+
public override void reset () {
168+
context?.reset ();
169+
maybe_request_surrounding ();
170+
171+
surrounding_text = null;
172+
surrounding_cursor = 0;
173+
surrounding_anchor = 0;
174+
preedit_text = null;
175+
}
176+
177+
public override void set_cursor_location (Graphene.Rect rect) {
178+
context?.set_cursor_location ((int) rect.origin.x, (int) rect.origin.y, (int) rect.size.width, (int) rect.size.height);
179+
cursor_location = rect;
180+
}
181+
182+
public override void set_surrounding (string text, uint cursor_index, uint anchor_index) {
183+
var update_caps = (surrounding_text == null) != (text == null);
184+
185+
surrounding_text = text;
186+
surrounding_cursor = cursor_index;
187+
surrounding_anchor = anchor_index;
188+
189+
if (update_caps) {
190+
update_capabilities ();
191+
}
192+
193+
if (text == null) {
194+
return;
195+
}
196+
197+
var ibus_text = new IBus.Text.from_string (text);
198+
context?.set_surrounding_text (ibus_text, cursor_index, anchor_index);
199+
}
200+
201+
public override bool filter_key_event (Clutter.Event event) {
202+
if (context == null) {
203+
return false;
204+
}
205+
206+
var state = (IBus.ModifierType) event.get_state ();
207+
208+
if (IBus.ModifierType.IGNORED_MASK in state) {
209+
return false;
210+
}
211+
212+
if (event.get_type () == Clutter.EventType.KEY_RELEASE) {
213+
state |= IBus.ModifierType.RELEASE_MASK;
214+
}
215+
216+
context.process_key_event_async.begin (
217+
event.get_key_symbol (), event.get_key_code () - 8, state, -1, null,
218+
(obj, res) => {
219+
try {
220+
var handled = context.process_key_event_async_finish (res);
221+
notify_key_event (event, handled);
222+
} catch (Error e) {
223+
warning ("Failed to process key event on IM: %s", e.message);
224+
}
225+
}
226+
);
227+
228+
return true;
229+
}
230+
231+
public override void update_content_hints (Clutter.InputContentHintFlags hints) {
232+
IBus.InputHints ibus_hints = 0;
233+
234+
if (COMPLETION in hints) {
235+
ibus_hints |= IBus.InputHints.WORD_COMPLETION;
236+
}
237+
238+
if (SPELLCHECK in hints) {
239+
ibus_hints |= IBus.InputHints.SPELLCHECK;
240+
}
241+
242+
if (AUTO_CAPITALIZATION in hints) {
243+
ibus_hints |= IBus.InputHints.UPPERCASE_SENTENCES;
244+
}
245+
246+
if (LOWERCASE in hints) {
247+
ibus_hints |= IBus.InputHints.LOWERCASE;
248+
}
249+
250+
if (UPPERCASE in hints) {
251+
ibus_hints |= IBus.InputHints.UPPERCASE_CHARS;
252+
}
253+
254+
if (TITLECASE in hints) {
255+
ibus_hints |= IBus.InputHints.UPPERCASE_WORDS;
256+
}
257+
258+
if (SENSITIVE_DATA in hints) {
259+
ibus_hints |= IBus.InputHints.PRIVATE;
260+
}
261+
262+
if (HIDDEN_TEXT in hints) {
263+
// TODO: Probably needs a newer version
264+
// ibus_hints |= IBus.InputHints.HIDDEN_TEXT;
265+
}
266+
267+
input_hints = ibus_hints;
268+
269+
context?.set_content_type (input_purpose, input_hints);
270+
}
271+
272+
public override void update_content_purpose (Clutter.InputContentPurpose purpose) {
273+
IBus.InputPurpose ibus_purpose;
274+
275+
switch (purpose) {
276+
case NORMAL:
277+
ibus_purpose = FREE_FORM;
278+
break;
279+
case ALPHA:
280+
ibus_purpose = ALPHA;
281+
break;
282+
case DIGITS:
283+
ibus_purpose = DIGITS;
284+
break;
285+
case NUMBER:
286+
ibus_purpose = NUMBER;
287+
break;
288+
case PHONE:
289+
ibus_purpose = PHONE;
290+
break;
291+
case URL:
292+
ibus_purpose = URL;
293+
break;
294+
case EMAIL:
295+
ibus_purpose = EMAIL;
296+
break;
297+
case NAME:
298+
ibus_purpose = NAME;
299+
break;
300+
case PASSWORD:
301+
ibus_purpose = PASSWORD;
302+
break;
303+
case TERMINAL:
304+
ibus_purpose = TERMINAL;
305+
break;
306+
default:
307+
warning ("Unknown input purpose: %d", purpose);
308+
ibus_purpose = FREE_FORM;
309+
break;
310+
}
311+
312+
input_purpose = ibus_purpose;
313+
314+
context?.set_content_type (input_purpose, input_hints);
315+
}
316+
}

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ gala_bin_sources = files(
44
'DBusAccelerator.vala',
55
'DaemonManager.vala',
66
'DesktopIntegration.vala',
7+
'InputMethod.vala',
78
'InternalUtils.vala',
89
'KeyboardManager.vala',
910
'Main.vala',

0 commit comments

Comments
 (0)