Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Install Dependencies
run: |
apt update
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
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
- name: Build
env:
DESTDIR: out
Expand All @@ -53,7 +53,7 @@ jobs:
- uses: actions/checkout@v6
- name: Install Dependencies
run: |
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
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
- name: Build
env:
DESTDIR: out
Expand All @@ -72,7 +72,7 @@ jobs:
run: |
zypper addrepo https://download.opensuse.org/repositories/X11:Pantheon/16.0/X11:Pantheon.repo
zypper --gpg-auto-import-keys refresh
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
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
- uses: actions/checkout@v6
- name: Build
env:
Expand Down
15 changes: 15 additions & 0 deletions daemon/IBus/Candidate.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright 2026 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

public class Gala.Daemon.Candidate : Object {
public string? label { get; construct; }
public string? candidate { get; construct; }

public Candidate (string? label, string? candidate) {
Object (label: label, candidate: candidate);
}
}
120 changes: 120 additions & 0 deletions daemon/IBus/CandidateArea.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright 2026 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

public class Gala.Daemon.CandidateArea : Granite.Bin {
private const string[] DEFAULT_LABELS = {
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f"
};

public IBus.PanelService service { get; construct; }

private ListStore model;
private Gtk.SingleSelection selection_model;
private Gtk.ListView list_view;

private Gtk.Button prev_page_button;
private Gtk.Button next_page_button;

private Granite.Box content_box;

public CandidateArea (IBus.PanelService service) {
Object (service: service);
}

construct {
model = new ListStore (typeof (Candidate));

selection_model = new Gtk.SingleSelection (model);

var factory = new Gtk.SignalListItemFactory ();
factory.setup.connect (on_setup);
factory.bind.connect (on_bind);

list_view = new Gtk.ListView (selection_model, factory);

prev_page_button = new Gtk.Button ();
prev_page_button.clicked.connect (service.page_up);

next_page_button = new Gtk.Button ();
next_page_button.clicked.connect (service.page_down);

var button_box = new Granite.Box (HORIZONTAL, LINKED) {
hexpand = true
};
button_box.append (prev_page_button);
button_box.append (next_page_button);

content_box = new Granite.Box (VERTICAL);
content_box.append (list_view);
content_box.append (button_box);

child = content_box;
}

private void on_setup (Object obj) {
var item = (Gtk.ListItem) obj;
item.child = new CandidateBox (service, item);
}

private void on_bind (Object obj) {
var item = (Gtk.ListItem) obj;
var candidate = (Candidate) item.item;

var box = (CandidateBox) item.child;
box.bind_candidate (candidate);
}

public void update (IBus.LookupTable table) {
model.remove_all ();

if (table.get_orientation () == IBus.Orientation.HORIZONTAL) {
update_orientation (HORIZONTAL);
} else { /* VERTICAL or SYSTEM */
update_orientation (VERTICAL);
}

var n_candidates = table.get_number_of_candidates ();
var page_size = table.get_page_size ();
var cursor_pos = table.get_cursor_pos ();
var page = (uint) (cursor_pos / page_size);

var start_index = page * page_size;
var end_index = uint.min (start_index + page_size, n_candidates);

for (uint i = start_index; i < end_index; i++) {
var label = table.get_label (i)?.text ?? (
i - start_index < DEFAULT_LABELS.length ? DEFAULT_LABELS[i - start_index] : null
);

var candidate = table.get_candidate (i)?.text;

model.append (new Candidate (label, candidate));
}

selection_model.selected = table.get_cursor_in_page ();

update_buttons (table.is_round (), page, (uint) ((n_candidates + page_size - 1) / page_size));
}

private void update_orientation (Gtk.Orientation orientation) {
content_box.orientation = orientation;
list_view.orientation = orientation;

if (orientation == HORIZONTAL) {
prev_page_button.icon_name = "go-previous";
next_page_button.icon_name = "go-next";
} else {
prev_page_button.icon_name = "go-up";
next_page_button.icon_name = "go-down";
}
}

private void update_buttons (bool wraps_around, uint page, uint n_pages) {
prev_page_button.sensitive = wraps_around || page > 0;
next_page_button.sensitive = wraps_around || page < n_pages - 1;
}
}
44 changes: 44 additions & 0 deletions daemon/IBus/CandidateBox.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2026 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

public class Gala.Daemon.CandidateBox : Granite.Bin {
public IBus.PanelService service { get; construct; }
public unowned Gtk.ListItem list_item { get; construct; }

private Gtk.Label label_label;
private Gtk.Label candidate_label;

public CandidateBox (IBus.PanelService service, Gtk.ListItem list_item) {
Object (service: service, list_item: list_item);
}

construct {
label_label = new Gtk.Label (null);
label_label.add_css_class (Granite.CssClass.DIM);

candidate_label = new Gtk.Label (null);

var content_box = new Granite.Box (HORIZONTAL, HALF);
content_box.append (label_label);
content_box.append (candidate_label);

child = content_box;

var gesture_click = new Gtk.GestureClick ();
gesture_click.released.connect (on_clicked);
add_controller (gesture_click);
}

private void on_clicked (Gtk.GestureClick gesture, int n_press, double x, double y) {
service.candidate_clicked (list_item.position, gesture.get_current_button (), gesture.get_current_event_state ());
}

public void bind_candidate (Candidate candidate) {
label_label.label = candidate.label;
candidate_label.label = candidate.candidate;
}
}
122 changes: 122 additions & 0 deletions daemon/IBus/IBusCandidateWindow.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright 2026 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

public class Gala.Daemon.IBusCandidateWindow : Gtk.Window {
public IBus.PanelService service { get; construct; }

private Gtk.Label preedit_text;
private Gtk.Label auxiliary_text;
private CandidateArea candidate_area;

public IBusCandidateWindow (IBus.PanelService service) {
Object (service: service);
}

construct {
preedit_text = new Gtk.Label (null) {
halign = START,
visible = false,
};

auxiliary_text = new Gtk.Label (null) {
halign = START,
visible = false,
};

candidate_area = new CandidateArea (service) {
hexpand = true,
visible = false,
};

var content_box = new Granite.Box (VERTICAL) {
margin_start = 6,
margin_end = 6,
margin_top = 6,
margin_bottom = 6,
};
content_box.append (preedit_text);
content_box.append (auxiliary_text);
content_box.append (candidate_area);

titlebar = new Gtk.Grid () { visible = false };
child = content_box;
title = "IBUS_CANDIDATE";
resizable = false;

service.show_preedit_text.connect (on_show_preedit_text);
service.hide_preedit_text.connect (on_hide_preedit_text);
service.update_preedit_text.connect (on_update_preedit_text);
service.show_auxiliary_text.connect (on_show_auxiliary_text);
service.hide_auxiliary_text.connect (on_hide_auxiliary_text);
service.update_auxiliary_text.connect (on_update_auxiliary_text);
service.show_lookup_table.connect (on_show_lookup_table);
service.hide_lookup_table.connect (on_hide_lookup_table);
service.update_lookup_table.connect (on_update_lookup_table);
service.focus_out.connect (hide);
}

private void update_visibility () {
var is_visible = preedit_text.visible || auxiliary_text.visible || candidate_area.visible;

if (is_visible) {
present ();
} else {
hide ();
}
}

private void on_show_preedit_text () {
preedit_text.visible = true;
update_visibility ();
}

private void on_hide_preedit_text () {
preedit_text.visible = false;
update_visibility ();
}

private void on_update_preedit_text (IBus.Text text, uint cursor_pos, bool visible) {
preedit_text.visible = visible;
preedit_text.label = text.text;

update_visibility ();
}

private void on_show_auxiliary_text () {
auxiliary_text.visible = true;
update_visibility ();
}

private void on_hide_auxiliary_text () {
auxiliary_text.visible = false;
update_visibility ();
}

private void on_update_auxiliary_text (IBus.Text text, bool visible) {
auxiliary_text.visible = visible;
auxiliary_text.label = text.text;

update_visibility ();
}

private void on_show_lookup_table () {
candidate_area.visible = true;
update_visibility ();
}

private void on_hide_lookup_table () {
candidate_area.visible = false;
update_visibility ();
}

private void on_update_lookup_table (IBus.LookupTable table, bool visible) {
candidate_area.visible = visible;
update_visibility ();

candidate_area.update (table);
}
}
37 changes: 37 additions & 0 deletions daemon/IBus/IBusService.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2026 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/

public class Gala.Daemon.IBusService : Object {
private IBus.Bus bus;
private IBus.PanelService service;
private IBusCandidateWindow candidate_window;

construct {
bus = new IBus.Bus.async ();
bus.connected.connect (on_connected);
}

private void on_connected () {
bus.request_name_async.begin (
IBus.SERVICE_PANEL, IBus.BusNameFlag.REPLACE_EXISTING, -1, null,
on_name_acquired
);
}

private void on_name_acquired (Object? obj, AsyncResult res) {
try {
bus.request_name_async_finish (res);
} catch (Error e) {
warning ("Failed to acquire bus name: %s", e.message);
return;
}

/* We need to go via Object.new because we need to pass construct properties */
service = (IBus.PanelService) Object.@new (typeof (IBus.PanelService), "connection", bus.get_connection (), "object-path", IBus.PATH_PANEL);
candidate_window = new IBusCandidateWindow (service);
}
}
7 changes: 7 additions & 0 deletions daemon/Main.vala
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
*/

public class Gala.Daemon.Application : Gtk.Application {
private IBusService ibus_service;

public Application () {
Object (application_id: "org.pantheon.gala.daemon");
}

construct {
ibus_service = new IBusService ();
}

public override void startup () {
base.startup ();

Expand Down Expand Up @@ -36,6 +42,7 @@ public class Gala.Daemon.Application : Gtk.Application {
Gtk.init ();

connection.register_object (object_path, new DBus ());
connection.register_object (object_path, new OSKManager ());

return true;
}
Expand Down
Loading
Loading