Skip to content

Commit d53db49

Browse files
committed
Daemon: Implement the Candidate Popup for IBus
1 parent 16dd66a commit d53db49

9 files changed

Lines changed: 381 additions & 1 deletion

File tree

daemon/IBus/Candidate.vala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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.Daemon.Candidate : Object {
9+
public string? label { get; construct; }
10+
public string? candidate { get; construct; }
11+
12+
public Candidate (string? label, string? candidate) {
13+
Object (label: label, candidate: candidate);
14+
}
15+
}

daemon/IBus/CandidateArea.vala

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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.Daemon.CandidateArea : Granite.Bin {
9+
private const string[] DEFAULT_LABELS = {
10+
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f"
11+
};
12+
13+
public IBus.PanelService service { get; construct; }
14+
15+
private ListStore model;
16+
private Gtk.SingleSelection selection_model;
17+
private Gtk.ListView list_view;
18+
19+
private Gtk.Button prev_page_button;
20+
private Gtk.Button next_page_button;
21+
private Granite.Box button_box;
22+
23+
private Granite.Box content_box;
24+
25+
public CandidateArea (IBus.PanelService service) {
26+
Object (service: service);
27+
}
28+
29+
construct {
30+
model = new ListStore (typeof (Candidate));
31+
32+
selection_model = new Gtk.SingleSelection (model);
33+
34+
var factory = new Gtk.SignalListItemFactory ();
35+
factory.setup.connect (on_setup);
36+
factory.bind.connect (on_bind);
37+
38+
list_view = new Gtk.ListView (selection_model, factory);
39+
40+
prev_page_button = new Gtk.Button ();
41+
prev_page_button.clicked.connect (service.page_up);
42+
43+
next_page_button = new Gtk.Button ();
44+
next_page_button.clicked.connect (service.page_down);
45+
46+
button_box = new Granite.Box (HORIZONTAL, LINKED) {
47+
hexpand = true
48+
};
49+
button_box.append (prev_page_button);
50+
button_box.append (next_page_button);
51+
52+
content_box = new Granite.Box (VERTICAL);
53+
content_box.append (list_view);
54+
content_box.append (button_box);
55+
56+
child = content_box;
57+
}
58+
59+
private void on_setup (Object obj) {
60+
var item = (Gtk.ListItem) obj;
61+
item.child = new CandidateBox (service, item);
62+
}
63+
64+
private void on_bind (Object obj) {
65+
var item = (Gtk.ListItem) obj;
66+
var candidate = (Candidate) item.item;
67+
68+
var box = (CandidateBox) item.child;
69+
box.set_candidate (candidate);
70+
}
71+
72+
public void update (IBus.LookupTable table) {
73+
model.remove_all ();
74+
75+
if (table.get_orientation () == IBus.Orientation.HORIZONTAL) {
76+
update_orientation (HORIZONTAL);
77+
} else { /* VERTICAL or SYSTEM */
78+
update_orientation (VERTICAL);
79+
}
80+
81+
var n_candidates = table.get_number_of_candidates ();
82+
var page_size = table.get_page_size ();
83+
84+
if (page_size == 0) {
85+
/* I don't think 0 is intended to happen so print a warning */
86+
warning ("LookupTable page size is 0, using 5");
87+
page_size = 5;
88+
}
89+
90+
var cursor_pos = table.get_cursor_pos ();
91+
var page = (uint) (cursor_pos / page_size);
92+
93+
var start_index = page * page_size;
94+
var end_index = uint.min (start_index + page_size, n_candidates);
95+
96+
for (uint i = start_index; i < end_index; i++) {
97+
var ibus_label = table.get_label (i)?.text;
98+
var label = ibus_label != null && ibus_label.strip () != "" ? ibus_label : (
99+
i - start_index < DEFAULT_LABELS.length ? DEFAULT_LABELS[i - start_index] : null
100+
);
101+
102+
var candidate = table.get_candidate (i)?.text;
103+
104+
model.append (new Candidate (label, candidate));
105+
}
106+
107+
selection_model.selected = table.get_cursor_in_page ();
108+
109+
update_buttons (table.is_round (), page, (uint) ((n_candidates + page_size - 1) / page_size));
110+
}
111+
112+
private void update_orientation (Gtk.Orientation orientation) {
113+
content_box.orientation = orientation;
114+
list_view.orientation = orientation;
115+
116+
if (orientation == HORIZONTAL) {
117+
prev_page_button.icon_name = "go-previous";
118+
next_page_button.icon_name = "go-next";
119+
} else {
120+
prev_page_button.icon_name = "go-up";
121+
next_page_button.icon_name = "go-down";
122+
}
123+
}
124+
125+
private void update_buttons (bool wraps_around, uint page, uint n_pages) {
126+
button_box.visible = n_pages > 1;
127+
128+
prev_page_button.sensitive = wraps_around || page > 0;
129+
next_page_button.sensitive = wraps_around || page < n_pages - 1;
130+
}
131+
}

daemon/IBus/CandidateBox.vala

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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.Daemon.CandidateBox : Granite.Bin {
9+
public IBus.PanelService service { get; construct; }
10+
public unowned Gtk.ListItem list_item { get; construct; }
11+
12+
private Gtk.Label label_label;
13+
private Gtk.Label candidate_label;
14+
15+
public CandidateBox (IBus.PanelService service, Gtk.ListItem list_item) {
16+
Object (service: service, list_item: list_item);
17+
}
18+
19+
construct {
20+
label_label = new Gtk.Label (null);
21+
label_label.add_css_class (Granite.CssClass.DIM);
22+
23+
candidate_label = new Gtk.Label (null);
24+
25+
var content_box = new Granite.Box (HORIZONTAL, HALF);
26+
content_box.append (label_label);
27+
content_box.append (candidate_label);
28+
29+
child = content_box;
30+
31+
var gesture_click = new Gtk.GestureClick ();
32+
gesture_click.released.connect (on_clicked);
33+
add_controller (gesture_click);
34+
}
35+
36+
private void on_clicked (Gtk.GestureClick gesture, int n_press, double x, double y) {
37+
service.candidate_clicked (list_item.position, gesture.get_current_button (), gesture.get_current_event_state ());
38+
}
39+
40+
public void set_candidate (Candidate candidate) {
41+
label_label.label = candidate.label;
42+
candidate_label.label = candidate.candidate;
43+
}
44+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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.Daemon.IBusCandidateWindow : Gtk.Window {
9+
public IBus.PanelService service { get; construct; }
10+
11+
private Gtk.Label preedit_text;
12+
private Gtk.Label auxiliary_text;
13+
private CandidateArea candidate_area;
14+
15+
public IBusCandidateWindow (IBus.PanelService service) {
16+
Object (service: service);
17+
}
18+
19+
construct {
20+
preedit_text = new Gtk.Label (null) {
21+
halign = START,
22+
visible = false,
23+
};
24+
25+
auxiliary_text = new Gtk.Label (null) {
26+
halign = START,
27+
visible = false,
28+
};
29+
30+
candidate_area = new CandidateArea (service) {
31+
hexpand = true,
32+
visible = false,
33+
};
34+
35+
var content_box = new Granite.Box (VERTICAL) {
36+
margin_start = 6,
37+
margin_end = 6,
38+
margin_top = 6,
39+
margin_bottom = 6,
40+
};
41+
content_box.append (preedit_text);
42+
content_box.append (auxiliary_text);
43+
content_box.append (candidate_area);
44+
45+
titlebar = new Gtk.Grid () { visible = false };
46+
child = content_box;
47+
/* Used to identify the window for correct positioning in the wm */
48+
title = "IBUS_CANDIDATE";
49+
resizable = false;
50+
51+
service.show_preedit_text.connect (on_show_preedit_text);
52+
service.hide_preedit_text.connect (on_hide_preedit_text);
53+
service.update_preedit_text.connect (on_update_preedit_text);
54+
service.show_auxiliary_text.connect (on_show_auxiliary_text);
55+
service.hide_auxiliary_text.connect (on_hide_auxiliary_text);
56+
service.update_auxiliary_text.connect (on_update_auxiliary_text);
57+
service.show_lookup_table.connect (on_show_lookup_table);
58+
service.hide_lookup_table.connect (on_hide_lookup_table);
59+
service.update_lookup_table.connect (on_update_lookup_table);
60+
service.focus_out.connect (hide);
61+
}
62+
63+
private void update_visibility () {
64+
var is_visible = preedit_text.visible || auxiliary_text.visible || candidate_area.visible;
65+
66+
if (is_visible) {
67+
present ();
68+
} else {
69+
hide ();
70+
}
71+
}
72+
73+
private void on_show_preedit_text () {
74+
preedit_text.visible = true;
75+
update_visibility ();
76+
}
77+
78+
private void on_hide_preedit_text () {
79+
preedit_text.visible = false;
80+
update_visibility ();
81+
}
82+
83+
private void on_update_preedit_text (IBus.Text text, uint cursor_pos, bool visible) {
84+
preedit_text.visible = visible;
85+
preedit_text.label = text.text;
86+
87+
update_visibility ();
88+
}
89+
90+
private void on_show_auxiliary_text () {
91+
auxiliary_text.visible = true;
92+
update_visibility ();
93+
}
94+
95+
private void on_hide_auxiliary_text () {
96+
auxiliary_text.visible = false;
97+
update_visibility ();
98+
}
99+
100+
private void on_update_auxiliary_text (IBus.Text text, bool visible) {
101+
auxiliary_text.visible = visible;
102+
auxiliary_text.label = text.text;
103+
104+
update_visibility ();
105+
}
106+
107+
private void on_show_lookup_table () {
108+
candidate_area.visible = true;
109+
update_visibility ();
110+
}
111+
112+
private void on_hide_lookup_table () {
113+
candidate_area.visible = false;
114+
update_visibility ();
115+
}
116+
117+
private void on_update_lookup_table (IBus.LookupTable table, bool visible) {
118+
candidate_area.visible = visible;
119+
update_visibility ();
120+
121+
candidate_area.update (table);
122+
}
123+
}

daemon/IBus/IBusService.vala

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.Daemon.IBusService : Object {
9+
private IBus.Bus bus;
10+
private IBus.PanelService service;
11+
private IBusCandidateWindow candidate_window;
12+
13+
construct {
14+
bus = new IBus.Bus.async ();
15+
bus.connected.connect (on_connected);
16+
}
17+
18+
private void on_connected () {
19+
bus.request_name_async.begin (
20+
IBus.SERVICE_PANEL, IBus.BusNameFlag.REPLACE_EXISTING, -1, null,
21+
on_name_acquired
22+
);
23+
}
24+
25+
private void on_name_acquired (Object? obj, AsyncResult res) {
26+
try {
27+
bus.request_name_async_finish (res);
28+
} catch (Error e) {
29+
warning ("Failed to acquire bus name: %s", e.message);
30+
return;
31+
}
32+
33+
/* We need to go via Object.new because we need to pass construct properties */
34+
service = (IBus.PanelService) Object.@new (typeof (IBus.PanelService), "connection", bus.get_connection (), "object-path", IBus.PATH_PANEL);
35+
candidate_window = new IBusCandidateWindow (service);
36+
}
37+
}

daemon/Main.vala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@
44
*/
55

66
public class Gala.Daemon.Application : Gtk.Application {
7+
private IBusService ibus_service;
8+
79
public Application () {
810
Object (application_id: "org.pantheon.gala.daemon");
911
}
1012

13+
construct {
14+
ibus_service = new IBusService ();
15+
}
16+
1117
public override void startup () {
1218
base.startup ();
1319

daemon/meson.build

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ gala_daemon_sources = files(
55
'MonitorLabel.vala',
66
'Window.vala',
77
'WindowMenu.vala',
8+
'IBus' / 'Candidate.vala',
9+
'IBus' / 'CandidateArea.vala',
10+
'IBus' / 'CandidateBox.vala',
11+
'IBus' / 'IBusService.vala',
12+
'IBus' / 'IBusCandidateWindow.vala'
813
)
914

1015
gtk4_dep = dependency('gtk4')
@@ -16,6 +21,6 @@ executable(
1621
gala_common_enums,
1722
config_header,
1823
gala_resources,
19-
dependencies: [gtk4_dep, granite7_dep],
24+
dependencies: [gtk4_dep, granite7_dep, ibus_dep],
2025
install: true
2126
)

0 commit comments

Comments
 (0)