From 511ac8a1093cb52316bd54bcabc1f145c6e14c96 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Thu, 26 Mar 2026 22:20:04 +0100 Subject: [PATCH 01/11] DaemonManager: Handle windows on title notify We will need to handle windows before they are shown for the ibus popup and osk to make sure they get added to the shell group when they are shown. --- src/DaemonManager.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DaemonManager.vala b/src/DaemonManager.vala index 7303efca3..fb11f3cd1 100644 --- a/src/DaemonManager.vala +++ b/src/DaemonManager.vala @@ -32,7 +32,7 @@ public class Gala.DaemonManager : GLib.Object { client = new ManagedClient (display, args); client.window_created.connect ((window) => { - window.shown.connect (handle_daemon_window); + window.notify["title"].connect ((obj, pspec) => handle_daemon_window ((Meta.Window) obj)); }); } From c167023dbd30f823c1d2ca889ff163bb975e01dd Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Wed, 25 Mar 2026 17:05:30 +0100 Subject: [PATCH 02/11] Implement the InputMethod --- .github/workflows/main.yml | 6 +- docs/meson.build | 1 + meson.build | 3 +- src/InputMethod.vala | 316 +++++++++++++++++++++++++++++++++++++ src/WindowManager.vala | 5 + src/meson.build | 1 + 6 files changed, 328 insertions(+), 4 deletions(-) create mode 100644 src/InputMethod.vala diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 201ea5393..6104f3a23 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 @@ -59,7 +59,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 @@ -78,7 +78,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: diff --git a/docs/meson.build b/docs/meson.build index 130ccc20d..4e0e537ce 100644 --- a/docs/meson.build +++ b/docs/meson.build @@ -43,6 +43,7 @@ all_doc_target = custom_target( '--pkg', 'atk-bridge-2.0', '--pkg', 'gnome-bg-4', '--pkg', 'gnome-desktop-4', + '--pkg', 'ibus-1.0', '--pkg', 'libsystemd', '--pkg', 'wayland-server', '--pkg', 'pantheon-desktop-shell', diff --git a/meson.build b/meson.build index 0e78f370d..bfd8e366a 100644 --- a/meson.build +++ b/meson.build @@ -70,6 +70,7 @@ gio_unix_dep = dependency('gio-unix-2.0', version: '>= @0@'.format(glib_version_ gmodule_dep = dependency('gmodule-2.0') gee_dep = dependency('gee-0.8') gnome_desktop_dep = dependency('gnome-desktop-4') +ibus_dep = dependency('ibus-1.0') gnome_bg_dep = dependency('gnome-bg-4') m_dep = cc.find_library('m', required: false) posix_dep = vala.find_library('posix', required: false) @@ -171,7 +172,7 @@ endif add_project_arguments(vala_flags, language: 'vala') add_project_link_arguments(['-Wl,-rpath,@0@'.format(mutter_typelib_dir)], language: 'c') -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] +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] if get_option('systemd') gala_base_dep += systemd_dep diff --git a/src/InputMethod.vala b/src/InputMethod.vala new file mode 100644 index 000000000..5b4aae258 --- /dev/null +++ b/src/InputMethod.vala @@ -0,0 +1,316 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.InputMethod : Clutter.InputMethod { + public Meta.Display display { private get; construct; } + public Graphene.Rect cursor_location { get; private set; } + + private IBus.Bus bus; + private IBus.InputContext? context; + + private bool preedit_visible; + private string? preedit_text; + private uint preedit_cursor; + private uint preedit_anchor; + private Clutter.PreeditResetMode preedit_mode; + + private string? surrounding_text; + private uint surrounding_cursor; + private uint surrounding_anchor; + + private IBus.InputPurpose input_purpose; + private IBus.InputHints input_hints; + + public InputMethod (Meta.Display display) { + Object (display: display); + } + + construct { + IBus.init (); + + bus = new IBus.Bus.async (); + bus.connected.connect (on_connected); + + if (bus.is_connected ()) { + on_connected (); + } + } + + private void on_connected () { + bus.create_input_context_async.begin ("gala", -1, null, on_input_context_created); + } + + private void on_input_context_created (Object? obj, AsyncResult res) { + try { + context = bus.create_input_context_async_finish (res); + } catch (Error e) { + warning ("Failed to create IBus input context: %s", e.message); + return; + } + + context.commit_text.connect (on_commit_text); + context.require_surrounding_text.connect (on_require_surrounding_text); + context.delete_surrounding_text.connect (on_delete_surrounding_text); + context.update_preedit_text.connect (on_update_preedit_text); + context.update_preedit_text_with_mode.connect (on_update_preedit_text_with_mode); + context.show_preedit_text.connect (on_show_preedit_text); + context.hide_preedit_text.connect (on_hide_preedit_text); + context.forward_key_event.connect (on_forward_key_event); + context.destroy.connect (on_destroy); + + update_capabilities (); + } + + private void update_capabilities () { + IBus.Capabilite caps = PREEDIT_TEXT | FOCUS; + + if (surrounding_text != null) { + caps |= SURROUNDING_TEXT; + } + + context?.set_capabilities (caps); + } + + private void on_commit_text (IBus.Text text) { + commit (text.text); + } + + private void on_require_surrounding_text () { + request_surrounding (); + } + + private void on_delete_surrounding_text (int offset, uint length) { + delete_surrounding (offset, length); + } + + private void on_update_preedit_text (IBus.Text text, uint cursor_pos, bool visible) { + on_update_preedit_text_with_mode (text, cursor_pos, visible, preedit_mode); + } + + private void on_update_preedit_text_with_mode (IBus.Text text, uint cursor_pos, bool visible, uint mode) { + var preedit = text.text; + + if (preedit == "") { + preedit = null; + } + + var anchor = cursor_pos; + + if (visible) { + set_preedit_text (preedit, cursor_pos, anchor, mode); + } else if (preedit_visible) { + set_preedit_text (null, cursor_pos, anchor, mode); + } + + preedit_visible = visible; + preedit_text = preedit; + preedit_cursor = cursor_pos; + preedit_anchor = anchor; + preedit_mode = (Clutter.PreeditResetMode) mode; + } + + private void on_show_preedit_text () { + preedit_visible = true; + set_preedit_text (preedit_text, preedit_cursor, preedit_anchor, preedit_mode); + } + + private void on_hide_preedit_text () { + set_preedit_text (null, preedit_cursor, preedit_anchor, preedit_mode); + preedit_visible = false; + } + + private void on_forward_key_event (uint keyval, uint keycode, uint _modifiers) { + var modifiers = (IBus.ModifierType) _modifiers; + var press = !(IBus.ModifierType.RELEASE_MASK in modifiers); + modifiers &= ~IBus.ModifierType.RELEASE_MASK; + + var time = display.get_current_time (); + + forward_key (keyval, keycode + 8, modifiers & Clutter.ModifierType.MODIFIER_MASK, time, press); + } + + private void on_destroy () { + debug ("IBus input context was destroyed"); + context = null; + } + + private void maybe_request_surrounding () { + if (context != null && context.needs_surrounding_text ()) { + request_surrounding (); + } + } + + public override void focus_in (Clutter.InputFocus actor) { + update_capabilities (); + context?.set_content_type (input_purpose, input_hints); + maybe_request_surrounding (); + + context?.focus_in (); + } + + public override void focus_out () { + context?.set_content_type (0, 0); + context?.reset (); + + context?.focus_out (); + + if (preedit_visible) { + set_preedit_text (null, preedit_cursor, preedit_anchor, preedit_mode); + preedit_text = null; + } + } + + public override void reset () { + context?.reset (); + maybe_request_surrounding (); + + surrounding_text = null; + surrounding_cursor = 0; + surrounding_anchor = 0; + preedit_text = null; + } + + public override void set_cursor_location (Graphene.Rect rect) { + context?.set_cursor_location ((int) rect.origin.x, (int) rect.origin.y, (int) rect.size.width, (int) rect.size.height); + cursor_location = rect; + } + + public override void set_surrounding (string text, uint cursor_index, uint anchor_index) { + var update_caps = (surrounding_text == null) != (text == null); + + surrounding_text = text; + surrounding_cursor = cursor_index; + surrounding_anchor = anchor_index; + + if (update_caps) { + update_capabilities (); + } + + if (text == null) { + return; + } + + var ibus_text = new IBus.Text.from_string (text); + context?.set_surrounding_text (ibus_text, cursor_index, anchor_index); + } + + public override bool filter_key_event (Clutter.Event event) { + if (context == null) { + return false; + } + + var state = (IBus.ModifierType) event.get_state (); + + if (IBus.ModifierType.IGNORED_MASK in state) { + return false; + } + + if (event.get_type () == Clutter.EventType.KEY_RELEASE) { + state |= IBus.ModifierType.RELEASE_MASK; + } + + context.process_key_event_async.begin ( + event.get_key_symbol (), event.get_key_code () - 8, state, -1, null, + (obj, res) => { + try { + var handled = context.process_key_event_async_finish (res); + notify_key_event (event, handled); + } catch (Error e) { + warning ("Failed to process key event on IM: %s", e.message); + } + } + ); + + return true; + } + + public override void update_content_hints (Clutter.InputContentHintFlags hints) { + IBus.InputHints ibus_hints = 0; + + if (COMPLETION in hints) { + ibus_hints |= IBus.InputHints.WORD_COMPLETION; + } + + if (SPELLCHECK in hints) { + ibus_hints |= IBus.InputHints.SPELLCHECK; + } + + if (AUTO_CAPITALIZATION in hints) { + ibus_hints |= IBus.InputHints.UPPERCASE_SENTENCES; + } + + if (LOWERCASE in hints) { + ibus_hints |= IBus.InputHints.LOWERCASE; + } + + if (UPPERCASE in hints) { + ibus_hints |= IBus.InputHints.UPPERCASE_CHARS; + } + + if (TITLECASE in hints) { + ibus_hints |= IBus.InputHints.UPPERCASE_WORDS; + } + + if (SENSITIVE_DATA in hints) { + ibus_hints |= IBus.InputHints.PRIVATE; + } + + if (HIDDEN_TEXT in hints) { + // TODO: Probably needs a newer version + // ibus_hints |= IBus.InputHints.HIDDEN_TEXT; + } + + input_hints = ibus_hints; + + context?.set_content_type (input_purpose, input_hints); + } + + public override void update_content_purpose (Clutter.InputContentPurpose purpose) { + IBus.InputPurpose ibus_purpose; + + switch (purpose) { + case NORMAL: + ibus_purpose = FREE_FORM; + break; + case ALPHA: + ibus_purpose = ALPHA; + break; + case DIGITS: + ibus_purpose = DIGITS; + break; + case NUMBER: + ibus_purpose = NUMBER; + break; + case PHONE: + ibus_purpose = PHONE; + break; + case URL: + ibus_purpose = URL; + break; + case EMAIL: + ibus_purpose = EMAIL; + break; + case NAME: + ibus_purpose = NAME; + break; + case PASSWORD: + ibus_purpose = PASSWORD; + break; + case TERMINAL: + ibus_purpose = TERMINAL; + break; + default: + warning ("Unknown input purpose: %d", purpose); + ibus_purpose = FREE_FORM; + break; + } + + input_purpose = ibus_purpose; + + context?.set_content_type (input_purpose, input_hints); + } +} diff --git a/src/WindowManager.vala b/src/WindowManager.vala index a7c6a1193..ca49923c3 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -85,6 +85,8 @@ namespace Gala { private KeyboardManager keyboard_manager; + private InputMethod input_method; + public WindowTracker? window_tracker { get; private set; } private WindowMover window_mover; @@ -133,6 +135,9 @@ namespace Gala { } public override void start () { + input_method = new InputMethod (get_display ()); + Clutter.get_default_backend ().set_input_method (input_method); + ShellClientsManager.init (this); BlurManager.init (this); daemon_manager = new DaemonManager (get_display ()); diff --git a/src/meson.build b/src/meson.build index aa42ee15e..a19bbccef 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,6 +4,7 @@ gala_bin_sources = files( 'DBusAccelerator.vala', 'DaemonManager.vala', 'DesktopIntegration.vala', + 'InputMethod.vala', 'InternalUtils.vala', 'KeyboardManager.vala', 'Main.vala', From a46020991232b03580d3c002d8195dd996e7663a Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Wed, 25 Mar 2026 17:07:59 +0100 Subject: [PATCH 03/11] ShellClients: Introduce an IBusCandidateWindow --- src/ShellClients/IBusCandidateWindow.vala | 23 +++++++++++++++++++++++ src/ShellClients/ShellClientsManager.vala | 19 ++++++++++++++----- src/WindowManager.vala | 2 +- src/meson.build | 1 + 4 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 src/ShellClients/IBusCandidateWindow.vala diff --git a/src/ShellClients/IBusCandidateWindow.vala b/src/ShellClients/IBusCandidateWindow.vala new file mode 100644 index 000000000..2e8c73d3a --- /dev/null +++ b/src/ShellClients/IBusCandidateWindow.vala @@ -0,0 +1,23 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.IBusCandidateWindow : PositionedWindow { + public InputMethod im { get; construct; } + + public IBusCandidateWindow (InputMethod im, Meta.Window window) { + Object (im: im, window: window); + } + + construct { + im.notify["cursor-location"].connect (position_window); + } + + protected override void get_window_position (Mtk.Rectangle window_rect, out int x, out int y) { + x = (int) (im.cursor_location.origin.x + im.cursor_location.size.width); + y = (int) (im.cursor_location.origin.y + im.cursor_location.size.height); + } +} diff --git a/src/ShellClients/ShellClientsManager.vala b/src/ShellClients/ShellClientsManager.vala index 84594cbad..c6f7b2e27 100644 --- a/src/ShellClients/ShellClientsManager.vala +++ b/src/ShellClients/ShellClientsManager.vala @@ -8,12 +8,12 @@ public class Gala.ShellClientsManager : Object, GestureTarget { private static ShellClientsManager instance; - public static void init (WindowManager wm) { + public static void init (WindowManager wm, InputMethod im) { if (instance != null) { return; } - instance = new ShellClientsManager (wm); + instance = new ShellClientsManager (wm, im); } public static unowned ShellClientsManager? get_instance () { @@ -21,6 +21,7 @@ public class Gala.ShellClientsManager : Object, GestureTarget { } public WindowManager wm { get; construct; } + public InputMethod im { get; construct; } private NotificationsClient notifications_client; private ManagedClient[] protocol_clients = {}; @@ -30,9 +31,10 @@ public class Gala.ShellClientsManager : Object, GestureTarget { private GLib.HashTable panel_windows = new GLib.HashTable (null, null); private GLib.HashTable positioned_windows = new GLib.HashTable (null, null); private GLib.HashTable monitor_label_windows = new GLib.HashTable (null, null); + private IBusCandidateWindow? ibus_candidate_window = null; - private ShellClientsManager (WindowManager wm) { - Object (wm: wm); + private ShellClientsManager (WindowManager wm, InputMethod im) { + Object (wm: wm, im: im); } construct { @@ -257,6 +259,12 @@ public class Gala.ShellClientsManager : Object, GestureTarget { window.unmanaging.connect_after ((_window) => monitor_label_windows.remove (_window)); } + public void make_ibus_candidate_window (Meta.Window window) requires (ibus_candidate_window == null) { + ibus_candidate_window = new IBusCandidateWindow (im, window); + + window.unmanaged.connect_after (() => ibus_candidate_window = null); + } + public void propagate (UpdateType update_type, GestureAction action, double progress) { foreach (var window in positioned_windows.get_values ()) { window.propagate (update_type, action, progress); @@ -272,7 +280,8 @@ public class Gala.ShellClientsManager : Object, GestureTarget { (window in positioned_windows && positioned_windows[window].modal) || (window in panel_windows) || (window in monitor_label_windows) || - NotificationStack.is_notification (window) + NotificationStack.is_notification (window) || + window == ibus_candidate_window?.window ); } diff --git a/src/WindowManager.vala b/src/WindowManager.vala index ca49923c3..ca9d60975 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -138,7 +138,7 @@ namespace Gala { input_method = new InputMethod (get_display ()); Clutter.get_default_backend ().set_input_method (input_method); - ShellClientsManager.init (this); + ShellClientsManager.init (this, input_method); BlurManager.init (this); daemon_manager = new DaemonManager (get_display ()); diff --git a/src/meson.build b/src/meson.build index a19bbccef..470aa1e81 100644 --- a/src/meson.build +++ b/src/meson.build @@ -46,6 +46,7 @@ gala_bin_sources = files( 'HotCorners/HotCornerManager.vala', 'ShellClients/ExtendedBehaviorWindow.vala', 'ShellClients/HideTracker.vala', + 'ShellClients/IBusCandidateWindow.vala', 'ShellClients/ManagedClient.vala', 'ShellClients/MonitorLabelWindow.vala', 'ShellClients/NotificationsClient.vala', From 7277bc08554045705cb11a250d32ca82fdbb56f3 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Wed, 25 Mar 2026 17:09:09 +0100 Subject: [PATCH 04/11] Daemon: Implement the Candidate Popup for IBus --- daemon/IBus/Candidate.vala | 15 +++ daemon/IBus/CandidateArea.vala | 131 +++++++++++++++++++++++++++ daemon/IBus/CandidateBox.vala | 44 +++++++++ daemon/IBus/IBusCandidateWindow.vala | 123 +++++++++++++++++++++++++ daemon/IBus/IBusService.vala | 37 ++++++++ daemon/Main.vala | 6 ++ daemon/meson.build | 7 +- data/gala-daemon.css | 10 ++ src/DaemonManager.vala | 9 ++ 9 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 daemon/IBus/Candidate.vala create mode 100644 daemon/IBus/CandidateArea.vala create mode 100644 daemon/IBus/CandidateBox.vala create mode 100644 daemon/IBus/IBusCandidateWindow.vala create mode 100644 daemon/IBus/IBusService.vala diff --git a/daemon/IBus/Candidate.vala b/daemon/IBus/Candidate.vala new file mode 100644 index 000000000..0a7217753 --- /dev/null +++ b/daemon/IBus/Candidate.vala @@ -0,0 +1,15 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +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); + } +} diff --git a/daemon/IBus/CandidateArea.vala b/daemon/IBus/CandidateArea.vala new file mode 100644 index 000000000..3f41e7257 --- /dev/null +++ b/daemon/IBus/CandidateArea.vala @@ -0,0 +1,131 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +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 button_box; + + 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); + + 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.set_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 (); + + if (page_size == 0) { + /* I don't think 0 is intended to happen so print a warning */ + warning ("LookupTable page size is 0, using 5"); + page_size = 5; + } + + 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 ibus_label = table.get_label (i)?.text; + var label = ibus_label != null && ibus_label.strip () != "" ? ibus_label : ( + 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) { + button_box.visible = n_pages > 1; + + prev_page_button.sensitive = wraps_around || page > 0; + next_page_button.sensitive = wraps_around || page < n_pages - 1; + } +} diff --git a/daemon/IBus/CandidateBox.vala b/daemon/IBus/CandidateBox.vala new file mode 100644 index 000000000..506225ebd --- /dev/null +++ b/daemon/IBus/CandidateBox.vala @@ -0,0 +1,44 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +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 set_candidate (Candidate candidate) { + label_label.label = candidate.label; + candidate_label.label = candidate.candidate; + } +} diff --git a/daemon/IBus/IBusCandidateWindow.vala b/daemon/IBus/IBusCandidateWindow.vala new file mode 100644 index 000000000..1d6dd9dc5 --- /dev/null +++ b/daemon/IBus/IBusCandidateWindow.vala @@ -0,0 +1,123 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +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; + /* Used to identify the window for correct positioning in the wm */ + 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); + } +} diff --git a/daemon/IBus/IBusService.vala b/daemon/IBus/IBusService.vala new file mode 100644 index 000000000..8d7fbd4c5 --- /dev/null +++ b/daemon/IBus/IBusService.vala @@ -0,0 +1,37 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +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); + } +} diff --git a/daemon/Main.vala b/daemon/Main.vala index bab809098..07a6c976d 100644 --- a/daemon/Main.vala +++ b/daemon/Main.vala @@ -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 (); diff --git a/daemon/meson.build b/daemon/meson.build index 671161ebb..cdee9dda7 100644 --- a/daemon/meson.build +++ b/daemon/meson.build @@ -5,6 +5,11 @@ gala_daemon_sources = files( 'MonitorLabel.vala', 'Window.vala', 'WindowMenu.vala', + 'IBus' / 'Candidate.vala', + 'IBus' / 'CandidateArea.vala', + 'IBus' / 'CandidateBox.vala', + 'IBus' / 'IBusService.vala', + 'IBus' / 'IBusCandidateWindow.vala' ) gtk4_dep = dependency('gtk4') @@ -16,6 +21,6 @@ executable( gala_common_enums, config_header, gala_resources, - dependencies: [gtk4_dep, granite7_dep], + dependencies: [gtk4_dep, granite7_dep, ibus_dep], install: true ) diff --git a/data/gala-daemon.css b/data/gala-daemon.css index 43a0f9eb1..bdb0fb1eb 100644 --- a/data/gala-daemon.css +++ b/data/gala-daemon.css @@ -16,3 +16,13 @@ daemon-window { margin: 1em; text-shadow: 0 1px 1px alpha(white, 0.1); } + +listview { + background: none; + border-spacing: 3px; +} + +listview > row { + padding: 3px; + border-radius: 4px; +} diff --git a/src/DaemonManager.vala b/src/DaemonManager.vala index fb11f3cd1..472bc0b67 100644 --- a/src/DaemonManager.vala +++ b/src/DaemonManager.vala @@ -32,6 +32,11 @@ public class Gala.DaemonManager : GLib.Object { client = new ManagedClient (display, args); client.window_created.connect ((window) => { +#if HAS_MUTTER49 + window.set_type (DOCK); +#elif HAS_MUTTER46 + client.wayland_client.make_dock (window); +#endif window.notify["title"].connect ((obj, pspec) => handle_daemon_window ((Meta.Window) obj)); }); } @@ -71,6 +76,10 @@ public class Gala.DaemonManager : GLib.Object { window.make_above (); window.stick (); break; + + case "IBUS_CANDIDATE": + ShellClientsManager.get_instance ().make_ibus_candidate_window (window); + break; } } From 0cdf96ac618d5e6967a51254f889f5c368239b11 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Thu, 26 Mar 2026 15:36:00 +0100 Subject: [PATCH 05/11] Implement an OSK manager --- src/DaemonManager.vala | 4 +- src/InputMethod.vala | 2 + src/OSK/OSKManager.vala | 79 ++++++++++++++++++++++++++++++++++++++++ src/OSK/OSKProxy.vala | 14 +++++++ src/OSK/OSKReceiver.vala | 38 +++++++++++++++++++ src/WindowManager.vala | 4 ++ src/meson.build | 3 ++ 7 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 src/OSK/OSKManager.vala create mode 100644 src/OSK/OSKProxy.vala create mode 100644 src/OSK/OSKReceiver.vala diff --git a/src/DaemonManager.vala b/src/DaemonManager.vala index 472bc0b67..8aab4066b 100644 --- a/src/DaemonManager.vala +++ b/src/DaemonManager.vala @@ -6,8 +6,8 @@ */ public class Gala.DaemonManager : GLib.Object { - private const string DAEMON_DBUS_NAME = "org.pantheon.gala.daemon"; - private const string DAEMON_DBUS_OBJECT_PATH = "/org/pantheon/gala/daemon"; + public const string DAEMON_DBUS_NAME = "org.pantheon.gala.daemon"; + public const string DAEMON_DBUS_OBJECT_PATH = "/org/pantheon/gala/daemon"; private const int SPACING = 12; [DBus (name = "org.pantheon.gala.daemon")] diff --git a/src/InputMethod.vala b/src/InputMethod.vala index 5b4aae258..b633e7b36 100644 --- a/src/InputMethod.vala +++ b/src/InputMethod.vala @@ -162,6 +162,8 @@ public class Gala.InputMethod : Clutter.InputMethod { set_preedit_text (null, preedit_cursor, preedit_anchor, preedit_mode); preedit_text = null; } + + set_input_panel_state (OFF); } public override void reset () { diff --git a/src/OSK/OSKManager.vala b/src/OSK/OSKManager.vala new file mode 100644 index 000000000..703a3e884 --- /dev/null +++ b/src/OSK/OSKManager.vala @@ -0,0 +1,79 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +/** + * Handles enabling/disabling and showing/hiding the on-screen keyboard (OSK). + */ +public class Gala.OSKManager : Object { + private const string OSK_BUS_NAME = "io.elementary.OSK"; + private const string OSK_OBJECT_PATH = "/io/elementary/OSK"; + + public Meta.Display display { private get; construct; } + public InputMethod im { private get; construct; } + + public int monitor { get; private set; default = 0; } + public bool visible { get; private set; default = false; } + + private OSKProxy? osk; + private OSKReceiver? receiver; + + private bool enabled = false; + + public OSKManager (Meta.Display display, InputMethod im) { + Object (display: display, im: im); + } + + construct { + Bus.watch_name (SESSION, OSK_BUS_NAME, NONE, () => osk_appeared.begin (), osk_lost); + + im.input_panel_state.connect (on_input_panel_state_changed); + + sync_enabled (); + } + + private async void osk_appeared () { + try { + osk = yield Bus.get_proxy (SESSION, OSK_BUS_NAME, OSK_OBJECT_PATH); + } catch (Error e) { + warning ("Failed to get OSK proxy: %s", e.message); + return; + } + + receiver = new OSKReceiver (display, osk, im); + + osk.set_enabled.begin (enabled); + } + + private void osk_lost () { + osk = null; + receiver = null; + } + + private void sync_enabled () { + enabled = true; + + if (osk != null) { + osk.set_enabled.begin (enabled); + } + } + + private void on_input_panel_state_changed (Clutter.InputPanelState state) { + switch (state) { + case ON: + visible = true; + break; + + case OFF: + visible = false; + break; + + case TOGGLE: + visible = !visible; + break; + } + } +} diff --git a/src/OSK/OSKProxy.vala b/src/OSK/OSKProxy.vala new file mode 100644 index 000000000..0dc4d8b81 --- /dev/null +++ b/src/OSK/OSKProxy.vala @@ -0,0 +1,14 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +[DBus (name = "io.elementary.OSK")] +public interface Gala.OSKProxy : Object { + public signal void keyval_pressed (uint keyval); + public signal void keyval_released (uint keyval); + + public async abstract void set_enabled (bool enabled) throws DBusError, IOError; +} diff --git a/src/OSK/OSKReceiver.vala b/src/OSK/OSKReceiver.vala new file mode 100644 index 000000000..5f1e83148 --- /dev/null +++ b/src/OSK/OSKReceiver.vala @@ -0,0 +1,38 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +/** + * Once the OSK is enabled the Manager creates a receiver which handles the input received from + * the OSK and forwards it. + */ +public class Gala.OSKReceiver : Object { + public Meta.Display display { private get; construct; } + public OSKProxy osk { private get; construct; } + public InputMethod im { private get; construct; } + + private Clutter.VirtualInputDevice virtual_device; + + public OSKReceiver (Meta.Display display, OSKProxy osk, InputMethod im) { + Object (display: display, osk: osk, im: im); + } + + construct { + var seat = Clutter.get_default_backend ().get_default_seat (); + virtual_device = seat.create_virtual_device (KEYBOARD_DEVICE); + + osk.keyval_pressed.connect (on_keyval_pressed); + osk.keyval_released.connect (on_keyval_released); + } + + private void on_keyval_pressed (uint keyval) { + virtual_device.notify_keyval (Clutter.get_current_event_time () * 1000, keyval, PRESSED); + } + + private void on_keyval_released (uint keyval) { + virtual_device.notify_keyval (Clutter.get_current_event_time () * 1000, keyval, RELEASED); + } +} diff --git a/src/WindowManager.vala b/src/WindowManager.vala index ca9d60975..7af468e1f 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -87,6 +87,8 @@ namespace Gala { private InputMethod input_method; + private OSKManager osk_manager; + public WindowTracker? window_tracker { get; private set; } private WindowMover window_mover; @@ -138,6 +140,8 @@ namespace Gala { input_method = new InputMethod (get_display ()); Clutter.get_default_backend ().set_input_method (input_method); + osk_manager = new OSKManager (get_display (), input_method); + ShellClientsManager.init (this, input_method); BlurManager.init (this); daemon_manager = new DaemonManager (get_display ()); diff --git a/src/meson.build b/src/meson.build index 470aa1e81..961cf015b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -44,6 +44,9 @@ gala_bin_sources = files( 'HotCorners/Barrier.vala', 'HotCorners/HotCorner.vala', 'HotCorners/HotCornerManager.vala', + 'OSK/OSKManager.vala', + 'OSK/OSKProxy.vala', + 'OSK/OSKReceiver.vala', 'ShellClients/ExtendedBehaviorWindow.vala', 'ShellClients/HideTracker.vala', 'ShellClients/IBusCandidateWindow.vala', From 52fc6360efe59bb1cc1fee7453ac44152e54218e Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Thu, 26 Mar 2026 16:27:21 +0100 Subject: [PATCH 06/11] ShellClients: Introduce an OSKWindow --- src/ShellClients/OSKWindow.vala | 54 +++++++++++++++++++++++ src/ShellClients/ShellClientsManager.vala | 19 +++++--- src/WindowManager.vala | 2 +- src/meson.build | 1 + 4 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 src/ShellClients/OSKWindow.vala diff --git a/src/ShellClients/OSKWindow.vala b/src/ShellClients/OSKWindow.vala new file mode 100644 index 000000000..7a0d2f048 --- /dev/null +++ b/src/ShellClients/OSKWindow.vala @@ -0,0 +1,54 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.OSKWindow : ShellWindow, RootTarget { + public OSKManager manager { private get; construct; } + + public Clutter.Actor? actor { get { return (Clutter.Actor) window.get_compositor_private (); } } + + private GestureController gesture_controller; + + public OSKWindow (OSKManager manager, Meta.Window window) { + Object (manager: manager, window: window); + } + + construct { + gesture_controller = new GestureController (CUSTOM) { + progress = 1 + }; + add_gesture_controller (gesture_controller); + + window.size_changed.connect (update_target); + window.shown.connect (update_target); + + manager.notify["visible"].connect (sync_visible); + window.shown.connect (sync_visible); + } + + private void update_target () { + var actor = (Clutter.Actor) window.get_compositor_private (); + hide_target = new PropertyTarget (CUSTOM, actor, "translation-y", typeof (float), 0f, actor.height); + } + + private void sync_visible () { + if (manager.visible) { + gesture_controller.goto (0); + } else { + gesture_controller.goto (1); + } + } + + protected override double get_hidden_progress () { + return gesture_controller.progress; + } + + protected override void get_window_position (Mtk.Rectangle window_rect, out int x, out int y) { + var monitor_geom = window.display.get_monitor_geometry (manager.monitor); + x = monitor_geom.x + (monitor_geom.width - window_rect.width) / 2; + y = monitor_geom.y + monitor_geom.height - window_rect.height; + } +} diff --git a/src/ShellClients/ShellClientsManager.vala b/src/ShellClients/ShellClientsManager.vala index c6f7b2e27..c46cba23c 100644 --- a/src/ShellClients/ShellClientsManager.vala +++ b/src/ShellClients/ShellClientsManager.vala @@ -8,12 +8,12 @@ public class Gala.ShellClientsManager : Object, GestureTarget { private static ShellClientsManager instance; - public static void init (WindowManager wm, InputMethod im) { + public static void init (WindowManager wm, InputMethod im, OSKManager osk_manager) { if (instance != null) { return; } - instance = new ShellClientsManager (wm, im); + instance = new ShellClientsManager (wm, im, osk_manager); } public static unowned ShellClientsManager? get_instance () { @@ -22,6 +22,7 @@ public class Gala.ShellClientsManager : Object, GestureTarget { public WindowManager wm { get; construct; } public InputMethod im { get; construct; } + public OSKManager osk_manager { get; construct; } private NotificationsClient notifications_client; private ManagedClient[] protocol_clients = {}; @@ -32,9 +33,10 @@ public class Gala.ShellClientsManager : Object, GestureTarget { private GLib.HashTable positioned_windows = new GLib.HashTable (null, null); private GLib.HashTable monitor_label_windows = new GLib.HashTable (null, null); private IBusCandidateWindow? ibus_candidate_window = null; + private OSKWindow? osk_window = null; - private ShellClientsManager (WindowManager wm, InputMethod im) { - Object (wm: wm, im: im); + private ShellClientsManager (WindowManager wm, InputMethod im, OSKManager osk_manager) { + Object (wm: wm, im: im, osk_manager: osk_manager); } construct { @@ -265,6 +267,12 @@ public class Gala.ShellClientsManager : Object, GestureTarget { window.unmanaged.connect_after (() => ibus_candidate_window = null); } + public void make_osk_window (Meta.Window window) requires (osk_window == null) { + osk_window = new OSKWindow (osk_manager, window); + + window.unmanaged.connect_after (() => osk_window = null); + } + public void propagate (UpdateType update_type, GestureAction action, double progress) { foreach (var window in positioned_windows.get_values ()) { window.propagate (update_type, action, progress); @@ -281,7 +289,8 @@ public class Gala.ShellClientsManager : Object, GestureTarget { (window in panel_windows) || (window in monitor_label_windows) || NotificationStack.is_notification (window) || - window == ibus_candidate_window?.window + window == ibus_candidate_window?.window || + window == osk_window?.window ); } diff --git a/src/WindowManager.vala b/src/WindowManager.vala index 7af468e1f..f687c6d4f 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -142,7 +142,7 @@ namespace Gala { osk_manager = new OSKManager (get_display (), input_method); - ShellClientsManager.init (this, input_method); + ShellClientsManager.init (this, input_method, osk_manager); BlurManager.init (this); daemon_manager = new DaemonManager (get_display ()); diff --git a/src/meson.build b/src/meson.build index 961cf015b..e789367dc 100644 --- a/src/meson.build +++ b/src/meson.build @@ -53,6 +53,7 @@ gala_bin_sources = files( 'ShellClients/ManagedClient.vala', 'ShellClients/MonitorLabelWindow.vala', 'ShellClients/NotificationsClient.vala', + 'ShellClients/OSKWindow.vala', 'ShellClients/PanelWindow.vala', 'ShellClients/PositionedWindow.vala', 'ShellClients/ShellClientsManager.vala', From b395bcced698c692ca73bd717b7670fb12f76bad Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 19 Apr 2026 10:49:50 +0100 Subject: [PATCH 07/11] WindowManager: Introduce own window group for OSK --- src/ShellClients/ShellClientsManager.vala | 4 ++++ src/WindowManager.vala | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/ShellClients/ShellClientsManager.vala b/src/ShellClients/ShellClientsManager.vala index c46cba23c..ea64bcec0 100644 --- a/src/ShellClients/ShellClientsManager.vala +++ b/src/ShellClients/ShellClientsManager.vala @@ -336,6 +336,10 @@ public class Gala.ShellClientsManager : Object, GestureTarget { return is_itself_system_modal (window) && positioned_windows[window].dim; } + public bool is_osk_window (Meta.Window window) { + return window.find_root_ancestor () == osk_window?.window; + } + //X11 only private void parse_mutter_hints (Meta.Window window) requires (!Meta.Util.is_wayland_compositor ()) { if (window.mutter_hints == null) { diff --git a/src/WindowManager.vala b/src/WindowManager.vala index f687c6d4f..cbea3fa24 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -51,6 +51,8 @@ namespace Gala { private Clutter.Actor menu_group { get; set; } + private Clutter.Actor osk_group; + /** * The group that contains all WindowActors that are system modal. * See {@link ShellClientsManager.is_system_modal_window}. @@ -241,6 +243,7 @@ namespace Gala { * +-- window overview * +-- shell group * +-- menu group + * +-- osk group * +-- modal group * +-- feedback group (e.g. DND icons) * +-- pointer locator @@ -309,6 +312,9 @@ namespace Gala { menu_group = new Clutter.Actor (); ui_group.add_child (menu_group); + osk_group = new Clutter.Actor (); + ui_group.add_child (osk_group); + modal_group = new ModalGroup (this, ShellClientsManager.get_instance ()); modal_group.add_constraint (new Clutter.BindConstraint (stage, SIZE, 0)); ui_group.add_child (modal_group); @@ -1057,6 +1063,11 @@ namespace Gala { return; } + if (ShellClientsManager.get_instance ().is_osk_window (window)) { + InternalUtils.clutter_actor_reparent (actor, osk_group); + return; + } + if (ShellClientsManager.get_instance ().is_shell_window (window)) { InternalUtils.clutter_actor_reparent (actor, shell_group); From 5cc31676221c3e06f76283b91603053648f08c69 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Thu, 26 Mar 2026 18:38:05 +0100 Subject: [PATCH 08/11] Daemon: Implement initial OSK --- daemon/Main.vala | 1 + daemon/OSK/Keyboard.vala | 55 +++++++++++++++++++++++++++++ daemon/OSK/OSKManager.vala | 37 ++++++++++++++++++++ daemon/OSK/OSKWindow.vala | 71 ++++++++++++++++++++++++++++++++++++++ daemon/meson.build | 8 +++-- src/DaemonManager.vala | 4 +++ 6 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 daemon/OSK/Keyboard.vala create mode 100644 daemon/OSK/OSKManager.vala create mode 100644 daemon/OSK/OSKWindow.vala diff --git a/daemon/Main.vala b/daemon/Main.vala index 07a6c976d..3c7a1791e 100644 --- a/daemon/Main.vala +++ b/daemon/Main.vala @@ -42,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; } diff --git a/daemon/OSK/Keyboard.vala b/daemon/OSK/Keyboard.vala new file mode 100644 index 000000000..1be362f91 --- /dev/null +++ b/daemon/OSK/Keyboard.vala @@ -0,0 +1,55 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.Keyboard : Granite.Bin { + public signal void key_clicked (uint keyval); + + construct { + var flowbox = new Gtk.FlowBox () { + min_children_per_line = 12, + }; + flowbox.append (create_button (Gdk.Key.A)); + flowbox.append (create_button (Gdk.Key.B)); + flowbox.append (create_button (Gdk.Key.C)); + flowbox.append (create_button (Gdk.Key.D)); + flowbox.append (create_button (Gdk.Key.E)); + flowbox.append (create_button (Gdk.Key.F)); + flowbox.append (create_button (Gdk.Key.G)); + flowbox.append (create_button (Gdk.Key.H)); + flowbox.append (create_button (Gdk.Key.I)); + flowbox.append (create_button (Gdk.Key.J)); + flowbox.append (create_button (Gdk.Key.K)); + flowbox.append (create_button (Gdk.Key.L)); + flowbox.append (create_button (Gdk.Key.M)); + flowbox.append (create_button (Gdk.Key.N)); + flowbox.append (create_button (Gdk.Key.O)); + flowbox.append (create_button (Gdk.Key.P)); + flowbox.append (create_button (Gdk.Key.Q)); + flowbox.append (create_button (Gdk.Key.R)); + flowbox.append (create_button (Gdk.Key.S)); + flowbox.append (create_button (Gdk.Key.T)); + flowbox.append (create_button (Gdk.Key.U)); + flowbox.append (create_button (Gdk.Key.V)); + flowbox.append (create_button (Gdk.Key.W)); + flowbox.append (create_button (Gdk.Key.X)); + flowbox.append (create_button (Gdk.Key.Y)); + flowbox.append (create_button (Gdk.Key.Z)); + flowbox.append (create_button (Gdk.Key.BackSpace)); + flowbox.append (create_button (Gdk.Key.space)); + flowbox.append (create_button (Gdk.Key.Escape)); + + child = flowbox; + } + + private Gtk.Button create_button (uint keyval) { + var button = new Gtk.Button.with_label (Gdk.keyval_name (keyval)) { + action_name = OSKWindow.ACTION_PREFIX + OSKWindow.ACTION_KEYVAL_CLICKED, + action_target = keyval + }; + return button; + } +} diff --git a/daemon/OSK/OSKManager.vala b/daemon/OSK/OSKManager.vala new file mode 100644 index 000000000..79d84a5cc --- /dev/null +++ b/daemon/OSK/OSKManager.vala @@ -0,0 +1,37 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +[DBus (name = "io.elementary.OSK")] +public class Gala.Daemon.OSKManager : Object { + public signal void keyval_pressed (uint keyval); + public signal void keyval_released (uint keyval); + + private OSKWindow? osk; + + public async void set_enabled (bool enabled) throws DBusError, IOError { + if (!enabled) { + osk?.destroy (); + osk = null; + return; + } + + osk = new OSKWindow (); + + osk.keyval_pressed.connect (on_keyval_pressed); + osk.keyval_released.connect (on_keyval_released); + + osk.present (); + } + + private void on_keyval_pressed (uint keyval) { + keyval_pressed (keyval); + } + + private void on_keyval_released (uint keyval) { + keyval_released (keyval); + } +} diff --git a/daemon/OSK/OSKWindow.vala b/daemon/OSK/OSKWindow.vala new file mode 100644 index 000000000..aee2d2227 --- /dev/null +++ b/daemon/OSK/OSKWindow.vala @@ -0,0 +1,71 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.OSKWindow : Gtk.Window { + public const string ACTION_GROUP_PREFIX = "osk"; + public const string ACTION_PREFIX = ACTION_GROUP_PREFIX + "."; + public const string ACTION_KEYVAL_PRESSED = "keyval-pressed"; + public const string ACTION_KEYVAL_RELEASED = "keyval-released"; + + /** + * Convenience for pressed + released + */ + public const string ACTION_KEYVAL_CLICKED = "keyval-clicked"; + + public signal void keyval_pressed (uint keyval); + public signal void keyval_released (uint keyval); + + construct { + var keyboard = new Keyboard (); + + child = keyboard; + titlebar = new Gtk.Grid () { visible = false }; + title = "OSK"; + + ((Gtk.Widget) this).realize.connect (update_size); + + var pressed_action = new SimpleAction (ACTION_KEYVAL_PRESSED, new VariantType ("u")); + pressed_action.activate.connect (on_keyval_pressed); + + var released_action = new SimpleAction (ACTION_KEYVAL_RELEASED, new VariantType ("u")); + released_action.activate.connect (on_keyval_released); + + var clicked_action = new SimpleAction (ACTION_KEYVAL_CLICKED, new VariantType ("u")); + clicked_action.activate.connect (on_keyval_clicked); + + var action_group = new SimpleActionGroup (); + action_group.add_action (pressed_action); + action_group.add_action (released_action); + action_group.add_action (clicked_action); + insert_action_group (ACTION_GROUP_PREFIX, action_group); + } + + private void update_size () { + var display = Gdk.Display.get_default (); + var monitor = display.get_monitor_at_surface (get_surface ()); + var monitor_geom = monitor.geometry; + + default_width = monitor_geom.width; + default_height = monitor_geom.height / 3; + } + + private void on_keyval_pressed (SimpleAction action, Variant? parameter) { + uint keyval = parameter.get_uint32 (); + keyval_pressed (keyval); + } + + private void on_keyval_released (SimpleAction action, Variant? parameter) { + uint keyval = parameter.get_uint32 (); + keyval_released (keyval); + } + + private void on_keyval_clicked (SimpleAction action, Variant? parameter) { + uint keyval = parameter.get_uint32 (); + keyval_pressed (keyval); + keyval_released (keyval); + } +} diff --git a/daemon/meson.build b/daemon/meson.build index cdee9dda7..27e367bc7 100644 --- a/daemon/meson.build +++ b/daemon/meson.build @@ -9,9 +9,13 @@ gala_daemon_sources = files( 'IBus' / 'CandidateArea.vala', 'IBus' / 'CandidateBox.vala', 'IBus' / 'IBusService.vala', - 'IBus' / 'IBusCandidateWindow.vala' + 'IBus' / 'IBusCandidateWindow.vala', + 'OSK' / 'Keyboard.vala', + 'OSK' / 'OSKManager.vala', + 'OSK' / 'OSKWindow.vala', ) +adw_dep = dependency('libadwaita-1') gtk4_dep = dependency('gtk4') granite7_dep = dependency('granite-7') @@ -21,6 +25,6 @@ executable( gala_common_enums, config_header, gala_resources, - dependencies: [gtk4_dep, granite7_dep, ibus_dep], + dependencies: [adw_dep, gtk4_dep, granite7_dep, ibus_dep], install: true ) diff --git a/src/DaemonManager.vala b/src/DaemonManager.vala index 8aab4066b..02ecd7ba1 100644 --- a/src/DaemonManager.vala +++ b/src/DaemonManager.vala @@ -80,6 +80,10 @@ public class Gala.DaemonManager : GLib.Object { case "IBUS_CANDIDATE": ShellClientsManager.get_instance ().make_ibus_candidate_window (window); break; + + case "OSK": + ShellClientsManager.get_instance ().make_osk_window (window); + break; } } From 08a56fabfee621b56b4dc941252b4604c292aa5b Mon Sep 17 00:00:00 2001 From: Leonhard Date: Thu, 9 Apr 2026 16:15:37 +0200 Subject: [PATCH 09/11] Flesh out OSK --- daemon/OSK/InputManager.vala | 24 +++ daemon/OSK/Keyboard.vala | 55 ------ daemon/OSK/Keyboard/KeyButton.vala | 29 ++++ daemon/OSK/Keyboard/Keyboard.vala | 94 +++++++++++ daemon/OSK/Keyboard/ViewContainer.vala | 78 +++++++++ daemon/OSK/KeyboardModel/Key.vala | 42 +++++ daemon/OSK/KeyboardModel/KeyboardModel.vala | 36 ++++ .../KeyboardModel/KeyboardModelBuilder.vala | 143 ++++++++++++++++ daemon/OSK/KeyboardModel/KeyboardView.vala | 16 ++ daemon/OSK/ModelManager.vala | 99 +++++++++++ daemon/OSK/OSKManager.vala | 15 +- daemon/OSK/OSKWindow.vala | 49 +----- daemon/OSK/Parsers/GnomeOSKParser.vala | 156 ++++++++++++++++++ daemon/meson.build | 14 +- 14 files changed, 740 insertions(+), 110 deletions(-) create mode 100644 daemon/OSK/InputManager.vala delete mode 100644 daemon/OSK/Keyboard.vala create mode 100644 daemon/OSK/Keyboard/KeyButton.vala create mode 100644 daemon/OSK/Keyboard/Keyboard.vala create mode 100644 daemon/OSK/Keyboard/ViewContainer.vala create mode 100644 daemon/OSK/KeyboardModel/Key.vala create mode 100644 daemon/OSK/KeyboardModel/KeyboardModel.vala create mode 100644 daemon/OSK/KeyboardModel/KeyboardModelBuilder.vala create mode 100644 daemon/OSK/KeyboardModel/KeyboardView.vala create mode 100644 daemon/OSK/ModelManager.vala create mode 100644 daemon/OSK/Parsers/GnomeOSKParser.vala diff --git a/daemon/OSK/InputManager.vala b/daemon/OSK/InputManager.vala new file mode 100644 index 000000000..3caf752e8 --- /dev/null +++ b/daemon/OSK/InputManager.vala @@ -0,0 +1,24 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.InputManager : Object { + public unowned OSKManager osk_manager { get; construct; } + + public InputManager (OSKManager osk_manager) { + Object (osk_manager: osk_manager); + } + + public void send_keyval (uint keyval) { + osk_manager.keyval_pressed (keyval); + osk_manager.keyval_released (keyval); + } + + public void erase () { + osk_manager.keyval_pressed (Gdk.Key.BackSpace); + osk_manager.keyval_released (Gdk.Key.BackSpace); + } +} diff --git a/daemon/OSK/Keyboard.vala b/daemon/OSK/Keyboard.vala deleted file mode 100644 index 1be362f91..000000000 --- a/daemon/OSK/Keyboard.vala +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2026 elementary, Inc. (https://elementary.io) - * SPDX-License-Identifier: GPL-3.0-or-later - * - * Authored by: Leonhard Kargl - */ - -public class Gala.Daemon.Keyboard : Granite.Bin { - public signal void key_clicked (uint keyval); - - construct { - var flowbox = new Gtk.FlowBox () { - min_children_per_line = 12, - }; - flowbox.append (create_button (Gdk.Key.A)); - flowbox.append (create_button (Gdk.Key.B)); - flowbox.append (create_button (Gdk.Key.C)); - flowbox.append (create_button (Gdk.Key.D)); - flowbox.append (create_button (Gdk.Key.E)); - flowbox.append (create_button (Gdk.Key.F)); - flowbox.append (create_button (Gdk.Key.G)); - flowbox.append (create_button (Gdk.Key.H)); - flowbox.append (create_button (Gdk.Key.I)); - flowbox.append (create_button (Gdk.Key.J)); - flowbox.append (create_button (Gdk.Key.K)); - flowbox.append (create_button (Gdk.Key.L)); - flowbox.append (create_button (Gdk.Key.M)); - flowbox.append (create_button (Gdk.Key.N)); - flowbox.append (create_button (Gdk.Key.O)); - flowbox.append (create_button (Gdk.Key.P)); - flowbox.append (create_button (Gdk.Key.Q)); - flowbox.append (create_button (Gdk.Key.R)); - flowbox.append (create_button (Gdk.Key.S)); - flowbox.append (create_button (Gdk.Key.T)); - flowbox.append (create_button (Gdk.Key.U)); - flowbox.append (create_button (Gdk.Key.V)); - flowbox.append (create_button (Gdk.Key.W)); - flowbox.append (create_button (Gdk.Key.X)); - flowbox.append (create_button (Gdk.Key.Y)); - flowbox.append (create_button (Gdk.Key.Z)); - flowbox.append (create_button (Gdk.Key.BackSpace)); - flowbox.append (create_button (Gdk.Key.space)); - flowbox.append (create_button (Gdk.Key.Escape)); - - child = flowbox; - } - - private Gtk.Button create_button (uint keyval) { - var button = new Gtk.Button.with_label (Gdk.keyval_name (keyval)) { - action_name = OSKWindow.ACTION_PREFIX + OSKWindow.ACTION_KEYVAL_CLICKED, - action_target = keyval - }; - return button; - } -} diff --git a/daemon/OSK/Keyboard/KeyButton.vala b/daemon/OSK/Keyboard/KeyButton.vala new file mode 100644 index 000000000..a8121cd37 --- /dev/null +++ b/daemon/OSK/Keyboard/KeyButton.vala @@ -0,0 +1,29 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.KeyButton : Granite.Bin { + public Key key { + set { + if (value.label != null) { + button.label = value.label; + } else if (value.icon != null) { + button.child = new Gtk.Image.from_gicon (value.icon); + } else { + button.label = _("Unknown Key"); + } + + button.set_detailed_action_name (value.detailed_action_name); + } + } + + private Gtk.Button button; + + construct { + button = new Gtk.Button (); + child = button; + } +} diff --git a/daemon/OSK/Keyboard/Keyboard.vala b/daemon/OSK/Keyboard/Keyboard.vala new file mode 100644 index 000000000..9fc756812 --- /dev/null +++ b/daemon/OSK/Keyboard/Keyboard.vala @@ -0,0 +1,94 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.Keyboard : Granite.Bin { + private const ActionEntry [] ACTIONS = { + { Key.ACTION_TYPE_KEY_VAL, on_type_key_val, "u" }, + { Key.ACTION_ERASE, on_erase }, + { Key.ACTION_SET_VIEW, on_set_view, "s" }, + { Key.ACTION_LATCH_VIEW, on_latch_view, "s" }, + }; + + public ModelManager model_manager { get; construct; } + public InputManager input_manager { get; construct; } + + private ViewContainer view_container; + + private KeyboardView? current_view; + /* Can be the same as current_view or a different one if another view is latched */ + private KeyboardView? _active_view; + private KeyboardView? active_view { + get { return _active_view; } + set { + _active_view = value; + + view_container.view = value?.rows; + } + } + + public Keyboard (ModelManager model_manager, InputManager input_manager) { + Object (model_manager: model_manager, input_manager: input_manager); + } + + construct { + view_container = new ViewContainer (); + child = view_container; + + var action_group = new SimpleActionGroup (); + action_group.add_action_entries (ACTIONS, this); + + insert_action_group (Key.ACTION_GROUP_PREFIX, action_group); + + model_manager.notify["current-model"].connect (on_current_model_changed); + } + + private void on_current_model_changed () { + current_view = model_manager.current_model?.find_default_view (); + active_view = current_view; + } + + private void on_type_key_val (SimpleAction action, Variant? param) { + var keyval = (uint) param.get_uint32 (); + + input_manager.send_keyval (keyval); + + if (active_view != current_view) { + /* Reset a latched view */ + active_view = current_view; + } + } + + private void on_erase () { + input_manager.erase (); + } + + private void on_set_view (SimpleAction action, Variant? param) { + var view_name = param.get_string (); + var view = model_manager.current_model.get_view_by_name (view_name); + + if (view == null) { + warning ("Tried to set view to '%s' but no such view exists", view_name); + return; + } + + current_view = view; + active_view = view; + } + + private void on_latch_view (SimpleAction action, Variant? param) { + var view_name = param.get_string (); + + var view = model_manager.current_model.get_view_by_name (view_name); + + if (view == null) { + warning ("Tried to latch view to '%s' but no such view exists", view_name); + return; + } + + active_view = view; + } +} diff --git a/daemon/OSK/Keyboard/ViewContainer.vala b/daemon/OSK/Keyboard/ViewContainer.vala new file mode 100644 index 000000000..e07e0ad46 --- /dev/null +++ b/daemon/OSK/Keyboard/ViewContainer.vala @@ -0,0 +1,78 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.ViewContainer : Granite.Bin { + private const int KEY_SIZE = 2; + + public ListModel view { set { update_keys (value); } } + + private Gtk.Grid grid; + private Gtk.AspectFrame aspect_frame; + + construct { + grid = new Gtk.Grid () { + row_homogeneous = true, + column_homogeneous = true, + row_spacing = 4, + column_spacing = 4, + }; + + aspect_frame = new Gtk.AspectFrame (0.5f, 0.5f, 1.0f, false) { + child = grid, + margin_top = 6, + margin_bottom = 6, + margin_start = 6, + margin_end = 6, + }; + + child = aspect_frame; + } + + private void update_keys (ListModel rows) { + while (grid.get_first_child () != null) { + grid.remove (grid.get_first_child ()); + } + + int max_row_width = 0; + int current_row = 0; + + for (int i = 0; i < rows.get_n_items (); i++) { + var row = (ListModel) rows.get_item (i); + + int row_width, row_height; + attach_row (current_row, row, out row_width, out row_height); + + max_row_width = int.max (max_row_width, row_width); + current_row += row_height; + } + + aspect_frame.ratio = (float) max_row_width / (float) current_row; + } + + private void attach_row (int index, ListModel row, out int row_width, out int row_height) { + row_width = 0; + row_height = 0; + + for (int i = 0; i < row.get_n_items (); i++) { + var key = (Key) row.get_item (i); + + var key_button = new KeyButton () { + key = key, + }; + + row_width += (int) (key.left_offset * KEY_SIZE); + + var width = (int) (key.width * KEY_SIZE); + var height = (int) (key.height * KEY_SIZE); + + grid.attach (key_button, row_width, index, width, height); + + row_width += width; + row_height = int.max (row_height, height); + } + } +} diff --git a/daemon/OSK/KeyboardModel/Key.vala b/daemon/OSK/KeyboardModel/Key.vala new file mode 100644 index 000000000..1c869604c --- /dev/null +++ b/daemon/OSK/KeyboardModel/Key.vala @@ -0,0 +1,42 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.Key : Object { + public const string ACTION_GROUP_PREFIX = "keyboard"; + public const string ACTION_PREFIX = ACTION_GROUP_PREFIX + "."; + /* Types the keyval given as the action target */ + public const string ACTION_TYPE_KEY_VAL = "keyval"; + /* Erases the last character */ + public const string ACTION_ERASE = "erase"; + /* Latches the keyboard view with the name given as the action target */ + public const string ACTION_LATCH_VIEW = "latch-view"; + /* Sets the keyboard view with the name given as the action target */ + public const string ACTION_SET_VIEW = "set-view"; + + public double left_offset { get; construct; default = 0.0; } + public double width { get; construct; default = 1.0; } + public double height { get; construct; default = 1.0; } + + public string detailed_action_name { get; construct; } + + public ListModel popup_keys { get; construct; } + + public string? label { get; construct; } + public Icon? icon { get; construct; } + + public Key (double left_offset, double width, double height, string detailed_action_name, ListModel popup_keys, string? label, Icon? icon) { + Object ( + left_offset: left_offset, + width: width, + height: height, + detailed_action_name: detailed_action_name, + popup_keys: popup_keys, + label: label, + icon: icon + ); + } +} diff --git a/daemon/OSK/KeyboardModel/KeyboardModel.vala b/daemon/OSK/KeyboardModel/KeyboardModel.vala new file mode 100644 index 000000000..b0241de22 --- /dev/null +++ b/daemon/OSK/KeyboardModel/KeyboardModel.vala @@ -0,0 +1,36 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.KeyboardModel : Object { + public ListModel views { get; construct; } + + public KeyboardModel (ListModel views) { + Object (views: views); + } + + public KeyboardView? get_view_by_name (string name) { + for (uint i = 0; i < views.get_n_items (); i++) { + var view = (KeyboardView) views.get_item (i); + if (view.name == name) { + return view; + } + } + + return null; + } + + public KeyboardView? find_default_view () { + for (uint i = 0; i < views.get_n_items (); i++) { + var view = (KeyboardView) views.get_item (i); + if (view.is_default) { + return view; + } + } + + return (KeyboardView?) views.get_item (0); + } +} diff --git a/daemon/OSK/KeyboardModel/KeyboardModelBuilder.vala b/daemon/OSK/KeyboardModel/KeyboardModelBuilder.vala new file mode 100644 index 000000000..1f8cc1319 --- /dev/null +++ b/daemon/OSK/KeyboardModel/KeyboardModelBuilder.vala @@ -0,0 +1,143 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.KeyboardModelBuilder : Object { + private KeyboardModel model; + private ListStore views_store; + + private string? current_view_name; + private ListStore? current_view_store; + private bool current_view_is_default = false; + + private ListStore? current_row; + + private double current_key_left_offset = 0.0; + private double current_key_width = 1.0; + private double current_key_height = 1.0; + private string? current_key_detailed_action_name; // Mandatory to set + private ListStore? current_key_popup_keys; + private string? current_key_label; + private Icon? current_key_icon; + + construct { + views_store = new ListStore (typeof (KeyboardView)); + model = new KeyboardModel (views_store); + } + + public KeyboardModel end () { + return model; + } + + public void begin_view (string name) requires (current_view_name == null && current_view_store == null) { + current_view_name = name; + current_view_store = new ListStore (typeof (ListStore)); + } + + public void set_view_default () requires (current_view_name != null && current_view_store != null) { + current_view_is_default = true; + } + + public void end_view () requires (current_view_name != null && current_view_store != null) { + var view = new KeyboardView (current_view_name, current_view_store, current_view_is_default); + views_store.append (view); + + + current_view_name = null; + current_view_store = null; + current_view_is_default = false; + } + + public void begin_row () requires (current_view_store != null && current_row == null) { + current_row = new ListStore (typeof (Key)); + } + + public void end_row () requires (current_view_store != null && current_row != null) { + current_view_store.append (current_row); + current_row = null; + } + + public void begin_key () requires (current_row != null && current_key_detailed_action_name == null) { + current_key_popup_keys = new ListStore (typeof (Key)); + } + + public void set_key_left_offset (double left_offset) { + current_key_left_offset = left_offset; + } + + public void set_key_width (double width) { + current_key_width = width; + } + + public void set_key_height (double height) { + current_key_height = height; + } + + public void set_key_val_action (uint val) { + current_key_detailed_action_name = Action.print_detailed_name (Key.ACTION_PREFIX + Key.ACTION_TYPE_KEY_VAL, new Variant.uint32 (val)); + } + + public void set_erase_action () { + current_key_detailed_action_name = Action.print_detailed_name (Key.ACTION_PREFIX + Key.ACTION_ERASE, null); + } + + public void set_latch_view_action (string view_name) { + current_key_detailed_action_name = Action.print_detailed_name (Key.ACTION_PREFIX + Key.ACTION_LATCH_VIEW, new Variant.string (view_name)); + } + + public void set_set_view_action (string view_name) { + current_key_detailed_action_name = Action.print_detailed_name (Key.ACTION_PREFIX + Key.ACTION_SET_VIEW, new Variant.string (view_name)); + } + + public void set_key_label (string label) { + current_key_label = label; + } + + public void set_key_icon (Icon icon) { + current_key_icon = icon; + } + + public void set_key_icon_name (string icon_name) { + current_key_icon = new ThemedIcon (icon_name); + } + + public void add_popup_key (string popup_key_string) requires (current_key_popup_keys != null) { + // var popup_key = new Key ( + // 1.0f, + // 1.0f, + // ACTION_PREFIX + "popup." + popup_key_string, + // null, + // popup_key_string, + // null + // ); + // current_key_popup_keys.append (popup_key); + } + + public void end_key () requires (current_row != null) { + if (current_key_label != null && current_key_icon != null) { + critical ("A key should have at least an icon or label."); + } + + var key = new Key ( + current_key_left_offset, + current_key_width, + current_key_height, + current_key_detailed_action_name ?? "none", + current_key_popup_keys, + current_key_label, + current_key_icon + ); + current_row.append (key); + + current_key_left_offset = 0.0; + current_key_width = 1.0; + current_key_height = 1.0; + current_key_detailed_action_name = null; + current_key_popup_keys = null; + current_key_label = null; + current_key_icon = null; + } +} diff --git a/daemon/OSK/KeyboardModel/KeyboardView.vala b/daemon/OSK/KeyboardModel/KeyboardView.vala new file mode 100644 index 000000000..e0f8ac2d0 --- /dev/null +++ b/daemon/OSK/KeyboardModel/KeyboardView.vala @@ -0,0 +1,16 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.KeyboardView : Object { + public string name { get; construct; } + public ListModel rows { get; construct; } + public bool is_default { get; construct; } + + public KeyboardView (string name, ListModel rows, bool is_default = false) { + Object (name: name, rows: rows, is_default: is_default); + } +} diff --git a/daemon/OSK/ModelManager.vala b/daemon/OSK/ModelManager.vala new file mode 100644 index 000000000..881a206aa --- /dev/null +++ b/daemon/OSK/ModelManager.vala @@ -0,0 +1,99 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.ModelManager : Object { + public IBus.InputPurpose input_purpose { private get; set; } + + public KeyboardModel? current_model { get; private set; } + + private Settings settings; + + construct { + settings = new Settings ("org.gnome.desktop.input-sources"); + settings.changed["current"].connect (update_model); + settings.changed["sources"].connect (update_model); + + update_model.begin (); + } + + private async void update_model () { + var input_sources = settings.get_value ("sources"); + + var current_source_index = settings.get_uint ("current"); + + var current_source = input_sources.get_child_value (current_source_index); + + string type, id; + current_source.get ("(ss)", out type, out id); + + var group_name = get_group_name (type, id); + + var eligible_models = get_eligible_models (group_name); + + foreach (var model in eligible_models) { + warning ("Load keyboard model: %s", model); + if (yield load_model (model)) { + return; + } + } + } + + private string get_group_name (string type, string id) { + return id; + } + + private string[] get_eligible_models (string current_group_name) { + switch (input_purpose) { + case DIGITS: + return { "digits" }; + case NUMBER: + return { "number" }; + case PHONE: + return { "phone" }; + case EMAIL: + return { "email" }; + case URL: + return { "url" }; + + default: + break; + } + + string[] groups = { current_group_name }; + + if ("+" in current_group_name) { + try { + groups += (/\+.*/).replace (current_group_name, current_group_name.length, 0, ""); + } catch (Error e) { + warning ("Failed to parse group name: %s", e.message); + } + } + + groups += "us"; + + if (input_purpose == TERMINAL) { + for (int i = 0; i < groups.length; i++) { + groups[i] += "-extended"; + } + } + + return groups; + } + + private async bool load_model (string name) { + var file = File.new_for_path ("/home/leonhard/Projects/gnome-shell/data/osk-layouts/%s.json".printf (name)); + var parser = new GnomeOSKParser (); + + try { + current_model = yield parser.parse (file); + return true; + } catch (Error e) { + warning ("Failed to load keyboard model: %s", e.message); + return false; + } + } +} diff --git a/daemon/OSK/OSKManager.vala b/daemon/OSK/OSKManager.vala index 79d84a5cc..09223993a 100644 --- a/daemon/OSK/OSKManager.vala +++ b/daemon/OSK/OSKManager.vala @@ -19,19 +19,14 @@ public class Gala.Daemon.OSKManager : Object { return; } - osk = new OSKWindow (); - - osk.keyval_pressed.connect (on_keyval_pressed); - osk.keyval_released.connect (on_keyval_released); + var model_manager = new ModelManager (); + var input_manager = new InputManager (this); + osk = new OSKWindow (model_manager, input_manager); osk.present (); } - private void on_keyval_pressed (uint keyval) { - keyval_pressed (keyval); - } - - private void on_keyval_released (uint keyval) { - keyval_released (keyval); + public async void set_input_purpose () throws DBusError, IOError { + // model_manager.set_input_purpose (input_purpose); } } diff --git a/daemon/OSK/OSKWindow.vala b/daemon/OSK/OSKWindow.vala index aee2d2227..150302043 100644 --- a/daemon/OSK/OSKWindow.vala +++ b/daemon/OSK/OSKWindow.vala @@ -6,42 +6,21 @@ */ public class Gala.Daemon.OSKWindow : Gtk.Window { - public const string ACTION_GROUP_PREFIX = "osk"; - public const string ACTION_PREFIX = ACTION_GROUP_PREFIX + "."; - public const string ACTION_KEYVAL_PRESSED = "keyval-pressed"; - public const string ACTION_KEYVAL_RELEASED = "keyval-released"; + public ModelManager model_manager { get; construct; } + public InputManager input_manager { get; construct; } - /** - * Convenience for pressed + released - */ - public const string ACTION_KEYVAL_CLICKED = "keyval-clicked"; - - public signal void keyval_pressed (uint keyval); - public signal void keyval_released (uint keyval); + public OSKWindow (ModelManager model_manager, InputManager input_manager) { + Object (model_manager: model_manager, input_manager: input_manager); + } construct { - var keyboard = new Keyboard (); + var keyboard = new Keyboard (model_manager, input_manager); child = keyboard; titlebar = new Gtk.Grid () { visible = false }; title = "OSK"; ((Gtk.Widget) this).realize.connect (update_size); - - var pressed_action = new SimpleAction (ACTION_KEYVAL_PRESSED, new VariantType ("u")); - pressed_action.activate.connect (on_keyval_pressed); - - var released_action = new SimpleAction (ACTION_KEYVAL_RELEASED, new VariantType ("u")); - released_action.activate.connect (on_keyval_released); - - var clicked_action = new SimpleAction (ACTION_KEYVAL_CLICKED, new VariantType ("u")); - clicked_action.activate.connect (on_keyval_clicked); - - var action_group = new SimpleActionGroup (); - action_group.add_action (pressed_action); - action_group.add_action (released_action); - action_group.add_action (clicked_action); - insert_action_group (ACTION_GROUP_PREFIX, action_group); } private void update_size () { @@ -52,20 +31,4 @@ public class Gala.Daemon.OSKWindow : Gtk.Window { default_width = monitor_geom.width; default_height = monitor_geom.height / 3; } - - private void on_keyval_pressed (SimpleAction action, Variant? parameter) { - uint keyval = parameter.get_uint32 (); - keyval_pressed (keyval); - } - - private void on_keyval_released (SimpleAction action, Variant? parameter) { - uint keyval = parameter.get_uint32 (); - keyval_released (keyval); - } - - private void on_keyval_clicked (SimpleAction action, Variant? parameter) { - uint keyval = parameter.get_uint32 (); - keyval_pressed (keyval); - keyval_released (keyval); - } } diff --git a/daemon/OSK/Parsers/GnomeOSKParser.vala b/daemon/OSK/Parsers/GnomeOSKParser.vala new file mode 100644 index 000000000..313f1caf3 --- /dev/null +++ b/daemon/OSK/Parsers/GnomeOSKParser.vala @@ -0,0 +1,156 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.GnomeOSKParser : Object { + private HashTable level_modes = new HashTable (str_hash, str_equal); + + public async KeyboardModel parse (File file) throws Error { + var stream = yield file.read_async (Priority.DEFAULT); + + var parser = new Json.Parser (); + yield parser.load_from_stream_async (stream, null); + + var root = parser.get_root ().get_object (); + var builder = new KeyboardModelBuilder (); + + var levels = root.get_array_member ("levels"); + parse_levels (levels, builder); + + return builder.end (); + } + + private void parse_levels (Json.Array json, KeyboardModelBuilder builder) throws Error { + level_modes.remove_all (); + + for (uint i = 0; i < json.get_length (); i++) { + var level_node = json.get_object_element (i); + parse_level (level_node, builder); + } + } + + private void parse_level (Json.Object json, KeyboardModelBuilder builder) throws Error { + var level_name = json.get_string_member ("level"); + + /* For GNOME every level has a set mode (locked or latched) but for us every level can be in every mode */ + var level_mode = json.get_string_member ("mode"); + level_modes[level_name] = level_mode; + + builder.begin_view (level_name); + + if (level_mode == "default") { + builder.set_view_default (); + } + + var rows = json.get_array_member ("rows"); + for (uint i = 0; i < rows.get_length (); i++) { + var row_node = rows.get_array_element (i); + parse_row (row_node, builder); + } + + builder.end_view (); + } + + private void parse_row (Json.Array json, KeyboardModelBuilder builder) throws Error { + builder.begin_row (); + + for (uint i = 0; i < json.get_length (); i++) { + var key_node = json.get_object_element (i); + parse_key (key_node, builder); + } + + builder.end_row (); + } + + private void parse_key (Json.Object json, KeyboardModelBuilder builder) throws Error { + builder.begin_key (); + + if (json.has_member ("strings")) { + var strings = json.get_array_member ("strings"); + for (uint i = 0; i < strings.get_length (); i++) { + var str = strings.get_string_element (i); + + if (i == 0) { + builder.set_key_val_action (Gdk.unicode_to_keyval (str[0])); + builder.set_key_label (str); + } else { + builder.add_popup_key (str); + } + } + } + + if (json.has_member ("leftOffset")) { + builder.set_key_left_offset (json.get_double_member ("leftOffset")); + } + + if (json.has_member ("width")) { + builder.set_key_width (json.get_double_member ("width")); + } + + if (json.has_member ("height")) { + builder.set_key_height (json.get_double_member ("height")); + } + + if (json.has_member ("label")) { + var label = json.get_string_member ("label"); + builder.set_key_label (label); + } + + if (json.has_member ("iconName")) { + var icon_name = json.get_string_member ("iconName"); + builder.set_key_icon_name (icon_name); + } + + if (json.has_member ("keyval")) { + var keyval = json.get_string_member ("keyval"); + builder.set_key_val_action (0); + // TODO: This is in hex so convert to uint + } + + if (json.has_member ("action")) { + var action = json.get_string_member ("action"); + switch (action) { + case "delete": + builder.set_erase_action (); + break; + + case "levelSwitch": + var level = json.get_string_member ("level"); + var level_mode = level_modes[level]; + + switch (level_mode) { + case null: + builder.set_set_view_action (level); + break; + + case "latched": + builder.set_latch_view_action (level); + break; + + case "default": + case "locked": + builder.set_set_view_action (level); + break; + } + break; + + case "emoji": + // TODO + break; + + case "languageMenu": + // TODO + break; + + case "hide": + // TODO + break; + } + } + + builder.end_key (); + } +} diff --git a/daemon/meson.build b/daemon/meson.build index 27e367bc7..af379fc18 100644 --- a/daemon/meson.build +++ b/daemon/meson.build @@ -10,7 +10,16 @@ gala_daemon_sources = files( 'IBus' / 'CandidateBox.vala', 'IBus' / 'IBusService.vala', 'IBus' / 'IBusCandidateWindow.vala', - 'OSK' / 'Keyboard.vala', + 'OSK' / 'Keyboard' / 'Keyboard.vala', + 'OSK' / 'Keyboard' / 'KeyButton.vala', + 'OSK' / 'Keyboard' / 'ViewContainer.vala', + 'OSK' / 'KeyboardModel' / 'Key.vala', + 'OSK' / 'KeyboardModel' / 'KeyboardModel.vala', + 'OSK' / 'KeyboardModel' / 'KeyboardModelBuilder.vala', + 'OSK' / 'KeyboardModel' / 'KeyboardView.vala', + 'OSK' / 'Parsers' / 'GnomeOSKParser.vala', + 'OSK' / 'InputManager.vala', + 'OSK' / 'ModelManager.vala', 'OSK' / 'OSKManager.vala', 'OSK' / 'OSKWindow.vala', ) @@ -18,6 +27,7 @@ gala_daemon_sources = files( adw_dep = dependency('libadwaita-1') gtk4_dep = dependency('gtk4') granite7_dep = dependency('granite-7') +json_dep = dependency('json-glib-1.0') executable( 'gala-daemon', @@ -25,6 +35,6 @@ executable( gala_common_enums, config_header, gala_resources, - dependencies: [adw_dep, gtk4_dep, granite7_dep, ibus_dep], + dependencies: [adw_dep, gtk4_dep, granite7_dep, ibus_dep, json_dep], install: true ) From 3601dc67bd2142c5d47f179d36e76011152a14e5 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 19 Apr 2026 10:18:06 +0100 Subject: [PATCH 10/11] Remodel the structure a bit in daemon --- daemon/Main.vala | 3 +- daemon/OSK/InputManager.vala | 14 +++--- daemon/OSK/ModelManager.vala | 10 ++-- daemon/OSK/OSKManager.vala | 49 +++++++++++++------ daemon/OSK/OSKService.vala | 24 +++++++++ .../OSK/{ => Window}/Keyboard/KeyButton.vala | 0 .../OSK/{ => Window}/Keyboard/Keyboard.vala | 1 + .../{ => Window}/Keyboard/ViewContainer.vala | 0 daemon/OSK/{ => Window}/OSKWindow.vala | 13 +++-- daemon/meson.build | 15 +++--- 10 files changed, 92 insertions(+), 37 deletions(-) create mode 100644 daemon/OSK/OSKService.vala rename daemon/OSK/{ => Window}/Keyboard/KeyButton.vala (100%) rename daemon/OSK/{ => Window}/Keyboard/Keyboard.vala (99%) rename daemon/OSK/{ => Window}/Keyboard/ViewContainer.vala (100%) rename daemon/OSK/{ => Window}/OSKWindow.vala (75%) diff --git a/daemon/Main.vala b/daemon/Main.vala index 3c7a1791e..d9c2b0f57 100644 --- a/daemon/Main.vala +++ b/daemon/Main.vala @@ -5,6 +5,7 @@ public class Gala.Daemon.Application : Gtk.Application { private IBusService ibus_service; + private OSKManager osk_manager; public Application () { Object (application_id: "org.pantheon.gala.daemon"); @@ -12,6 +13,7 @@ public class Gala.Daemon.Application : Gtk.Application { construct { ibus_service = new IBusService (); + osk_manager = new OSKManager (ibus_service); } public override void startup () { @@ -42,7 +44,6 @@ 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; } diff --git a/daemon/OSK/InputManager.vala b/daemon/OSK/InputManager.vala index 3caf752e8..9c25f3a18 100644 --- a/daemon/OSK/InputManager.vala +++ b/daemon/OSK/InputManager.vala @@ -6,19 +6,19 @@ */ public class Gala.Daemon.InputManager : Object { - public unowned OSKManager osk_manager { get; construct; } + public OSKService service { private get; construct; } - public InputManager (OSKManager osk_manager) { - Object (osk_manager: osk_manager); + public InputManager (OSKService service) { + Object (service: service); } public void send_keyval (uint keyval) { - osk_manager.keyval_pressed (keyval); - osk_manager.keyval_released (keyval); + service.keyval_pressed (keyval); + service.keyval_released (keyval); } public void erase () { - osk_manager.keyval_pressed (Gdk.Key.BackSpace); - osk_manager.keyval_released (Gdk.Key.BackSpace); + service.keyval_pressed (Gdk.Key.BackSpace); + service.keyval_released (Gdk.Key.BackSpace); } } diff --git a/daemon/OSK/ModelManager.vala b/daemon/OSK/ModelManager.vala index 881a206aa..8ac70a018 100644 --- a/daemon/OSK/ModelManager.vala +++ b/daemon/OSK/ModelManager.vala @@ -6,12 +6,16 @@ */ public class Gala.Daemon.ModelManager : Object { - public IBus.InputPurpose input_purpose { private get; set; } + public OSKService service { private get; construct; } public KeyboardModel? current_model { get; private set; } private Settings settings; + public ModelManager (OSKService service) { + Object (service: service); + } + construct { settings = new Settings ("org.gnome.desktop.input-sources"); settings.changed["current"].connect (update_model); @@ -47,7 +51,7 @@ public class Gala.Daemon.ModelManager : Object { } private string[] get_eligible_models (string current_group_name) { - switch (input_purpose) { + switch (service.osk_input_purpose) { case DIGITS: return { "digits" }; case NUMBER: @@ -75,7 +79,7 @@ public class Gala.Daemon.ModelManager : Object { groups += "us"; - if (input_purpose == TERMINAL) { + if (service.osk_input_purpose == TERMINAL) { for (int i = 0; i < groups.length; i++) { groups[i] += "-extended"; } diff --git a/daemon/OSK/OSKManager.vala b/daemon/OSK/OSKManager.vala index 09223993a..08d8ee07e 100644 --- a/daemon/OSK/OSKManager.vala +++ b/daemon/OSK/OSKManager.vala @@ -5,28 +5,45 @@ * Authored by: Leonhard Kargl */ -[DBus (name = "io.elementary.OSK")] public class Gala.Daemon.OSKManager : Object { - public signal void keyval_pressed (uint keyval); - public signal void keyval_released (uint keyval); + public IBusService ibus_service { private get; construct; } - private OSKWindow? osk; + private OSKService osk_service; + private OSKWindow? osk_window; - public async void set_enabled (bool enabled) throws DBusError, IOError { - if (!enabled) { - osk?.destroy (); - osk = null; - return; - } + public OSKManager (IBusService ibus_service) { + Object (ibus_service: ibus_service); + } - var model_manager = new ModelManager (); - var input_manager = new InputManager (this); + construct { + osk_service = new OSKService (); + osk_service.notify["osk-enabled"].connect (on_osk_enabled_changed); - osk = new OSKWindow (model_manager, input_manager); - osk.present (); + Bus.own_name (SESSION, "io.elementary.OSK", NONE, null, on_name_acquired); + } + + private void on_name_acquired (DBusConnection connection, string name) { + try { + connection.register_object ("/io/elementary/OSK", osk_service); + } catch (Error e) { + warning ("Failed to get D-Bus session bus: %s", e.message); + } } - public async void set_input_purpose () throws DBusError, IOError { - // model_manager.set_input_purpose (input_purpose); + private void on_osk_enabled_changed () { + /* If the OSK is active we show the candidates directly in the OSK so disable the popup */ + ibus_service.disable_popup = osk_service.osk_enabled; + + if (!osk_service.osk_enabled) { + osk_window?.destroy (); + osk_window = null; + return; + } + + var model_manager = new ModelManager (osk_service); + var input_manager = new InputManager (osk_service); + + osk_window = new OSKWindow (model_manager, input_manager, ibus_service); + osk_window.present (); } } diff --git a/daemon/OSK/OSKService.vala b/daemon/OSK/OSKService.vala new file mode 100644 index 000000000..af25cfab1 --- /dev/null +++ b/daemon/OSK/OSKService.vala @@ -0,0 +1,24 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +[DBus (name = "io.elementary.OSK")] +public class Gala.Daemon.OSKService : Object { + public signal void keyval_pressed (uint keyval); + public signal void keyval_released (uint keyval); + + internal bool osk_enabled { get; private set; } + internal IBus.InputPurpose osk_input_purpose { get; private set; } + + public void set_enabled (bool enabled) throws DBusError, IOError { + warning ("Set enabled"); + osk_enabled = enabled; + } + + public void set_input_purpose (IBus.InputPurpose input_purpose) throws DBusError, IOError { + osk_input_purpose = input_purpose; + } +} diff --git a/daemon/OSK/Keyboard/KeyButton.vala b/daemon/OSK/Window/Keyboard/KeyButton.vala similarity index 100% rename from daemon/OSK/Keyboard/KeyButton.vala rename to daemon/OSK/Window/Keyboard/KeyButton.vala diff --git a/daemon/OSK/Keyboard/Keyboard.vala b/daemon/OSK/Window/Keyboard/Keyboard.vala similarity index 99% rename from daemon/OSK/Keyboard/Keyboard.vala rename to daemon/OSK/Window/Keyboard/Keyboard.vala index 9fc756812..03711cf0c 100644 --- a/daemon/OSK/Keyboard/Keyboard.vala +++ b/daemon/OSK/Window/Keyboard/Keyboard.vala @@ -37,6 +37,7 @@ public class Gala.Daemon.Keyboard : Granite.Bin { construct { view_container = new ViewContainer (); child = view_container; + vexpand = true; var action_group = new SimpleActionGroup (); action_group.add_action_entries (ACTIONS, this); diff --git a/daemon/OSK/Keyboard/ViewContainer.vala b/daemon/OSK/Window/Keyboard/ViewContainer.vala similarity index 100% rename from daemon/OSK/Keyboard/ViewContainer.vala rename to daemon/OSK/Window/Keyboard/ViewContainer.vala diff --git a/daemon/OSK/OSKWindow.vala b/daemon/OSK/Window/OSKWindow.vala similarity index 75% rename from daemon/OSK/OSKWindow.vala rename to daemon/OSK/Window/OSKWindow.vala index 150302043..85aa0fa3f 100644 --- a/daemon/OSK/OSKWindow.vala +++ b/daemon/OSK/Window/OSKWindow.vala @@ -8,15 +8,22 @@ public class Gala.Daemon.OSKWindow : Gtk.Window { public ModelManager model_manager { get; construct; } public InputManager input_manager { get; construct; } + public IBusService ibus_service { get; construct; } - public OSKWindow (ModelManager model_manager, InputManager input_manager) { - Object (model_manager: model_manager, input_manager: input_manager); + public OSKWindow (ModelManager model_manager, InputManager input_manager, IBusService ibus_service) { + Object (model_manager: model_manager, input_manager: input_manager, ibus_service: ibus_service); } construct { + var suggestions = new Suggestions (ibus_service); + var keyboard = new Keyboard (model_manager, input_manager); - child = keyboard; + var box = new Granite.Box (VERTICAL); + box.append (suggestions); + box.append (keyboard); + + child = box; titlebar = new Gtk.Grid () { visible = false }; title = "OSK"; diff --git a/daemon/meson.build b/daemon/meson.build index af379fc18..fbd26b94f 100644 --- a/daemon/meson.build +++ b/daemon/meson.build @@ -10,18 +10,19 @@ gala_daemon_sources = files( 'IBus' / 'CandidateBox.vala', 'IBus' / 'IBusService.vala', 'IBus' / 'IBusCandidateWindow.vala', - 'OSK' / 'Keyboard' / 'Keyboard.vala', - 'OSK' / 'Keyboard' / 'KeyButton.vala', - 'OSK' / 'Keyboard' / 'ViewContainer.vala', + 'OSK' / 'InputManager.vala', + 'OSK' / 'ModelManager.vala', + 'OSK' / 'OSKManager.vala', + 'OSK' / 'OSKService.vala', 'OSK' / 'KeyboardModel' / 'Key.vala', 'OSK' / 'KeyboardModel' / 'KeyboardModel.vala', 'OSK' / 'KeyboardModel' / 'KeyboardModelBuilder.vala', 'OSK' / 'KeyboardModel' / 'KeyboardView.vala', 'OSK' / 'Parsers' / 'GnomeOSKParser.vala', - 'OSK' / 'InputManager.vala', - 'OSK' / 'ModelManager.vala', - 'OSK' / 'OSKManager.vala', - 'OSK' / 'OSKWindow.vala', + 'OSK' / 'Window' / 'OSKWindow.vala', + 'OSK' / 'Window' / 'Keyboard' / 'Keyboard.vala', + 'OSK' / 'Window' / 'Keyboard' / 'KeyButton.vala', + 'OSK' / 'Window' / 'Keyboard' / 'ViewContainer.vala', ) adw_dep = dependency('libadwaita-1') From 5356643ea7bf6ae61e8958ffe092337d979107d5 Mon Sep 17 00:00:00 2001 From: Leonhard Date: Sun, 19 Apr 2026 10:18:28 +0100 Subject: [PATCH 11/11] Add suggestions to the osk --- daemon/IBus/IBusCandidateWindow.vala | 4 ++- daemon/IBus/IBusService.vala | 38 +++++++++++++++++++++++++- daemon/OSK/Window/Suggestions.vala | 41 ++++++++++++++++++++++++++++ daemon/meson.build | 1 + 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 daemon/OSK/Window/Suggestions.vala diff --git a/daemon/IBus/IBusCandidateWindow.vala b/daemon/IBus/IBusCandidateWindow.vala index 1d6dd9dc5..62c534749 100644 --- a/daemon/IBus/IBusCandidateWindow.vala +++ b/daemon/IBus/IBusCandidateWindow.vala @@ -8,6 +8,8 @@ public class Gala.Daemon.IBusCandidateWindow : Gtk.Window { public IBus.PanelService service { get; construct; } + public bool disabled { get; set; default = false; } + private Gtk.Label preedit_text; private Gtk.Label auxiliary_text; private CandidateArea candidate_area; @@ -61,7 +63,7 @@ public class Gala.Daemon.IBusCandidateWindow : Gtk.Window { } private void update_visibility () { - var is_visible = preedit_text.visible || auxiliary_text.visible || candidate_area.visible; + var is_visible = !disabled && (preedit_text.visible || auxiliary_text.visible || candidate_area.visible); if (is_visible) { present (); diff --git a/daemon/IBus/IBusService.vala b/daemon/IBus/IBusService.vala index 8d7fbd4c5..c4b056d80 100644 --- a/daemon/IBus/IBusService.vala +++ b/daemon/IBus/IBusService.vala @@ -6,11 +6,18 @@ */ public class Gala.Daemon.IBusService : Object { + private ListStore _candidates; + public ListModel candidates { get { return _candidates; } } + + public bool disable_popup { get; set; default = false; } + public IBus.PanelService service { get; private set; } + private IBus.Bus bus; - private IBus.PanelService service; private IBusCandidateWindow candidate_window; construct { + _candidates = new ListStore (typeof (Candidate)); + bus = new IBus.Bus.async (); bus.connected.connect (on_connected); } @@ -33,5 +40,34 @@ public class Gala.Daemon.IBusService : Object { /* 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); + bind_property ("disable-popup", candidate_window, "disabled", SYNC_CREATE); + + service.update_lookup_table.connect (on_update_lookup_table); + } + + private void on_update_lookup_table (IBus.LookupTable table) { + _candidates.remove_all (); + + var n_candidates = table.get_number_of_candidates (); + var page_size = table.get_page_size (); + + if (page_size == 0) { + /* I don't think 0 is intended to happen so print a warning */ + warning ("LookupTable page size is 0, using 5"); + page_size = 5; + } + + 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; + var candidate = table.get_candidate (i)?.text; + + _candidates.append (new Candidate (label, candidate)); + } } } diff --git a/daemon/OSK/Window/Suggestions.vala b/daemon/OSK/Window/Suggestions.vala new file mode 100644 index 000000000..7ba0d6244 --- /dev/null +++ b/daemon/OSK/Window/Suggestions.vala @@ -0,0 +1,41 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.Daemon.Suggestions : Granite.Bin { + public IBusService ibus_service { private get; construct; } + + public Suggestions (IBusService ibus_service) { + Object (ibus_service: ibus_service); + } + + construct { + var selection_model = new Gtk.NoSelection (ibus_service.candidates); + + var factory = new Gtk.SignalListItemFactory (); + factory.setup.connect (on_setup); + factory.bind.connect (on_bind); + + var list_view = new Gtk.ListView (selection_model, factory) { + orientation = HORIZONTAL, + }; + child = list_view; + halign = CENTER; + } + + private void on_setup (Object obj) { + var item = (Gtk.ListItem) obj; + item.child = new CandidateBox (ibus_service.service, item); + } + + private void on_bind (Object obj) { + var item = (Gtk.ListItem) obj; + var candidate = (Candidate) item.item; + + var box = (CandidateBox) item.child; + box.set_candidate (candidate); + } +} diff --git a/daemon/meson.build b/daemon/meson.build index fbd26b94f..465ddb59c 100644 --- a/daemon/meson.build +++ b/daemon/meson.build @@ -20,6 +20,7 @@ gala_daemon_sources = files( 'OSK' / 'KeyboardModel' / 'KeyboardView.vala', 'OSK' / 'Parsers' / 'GnomeOSKParser.vala', 'OSK' / 'Window' / 'OSKWindow.vala', + 'OSK' / 'Window' / 'Suggestions.vala', 'OSK' / 'Window' / 'Keyboard' / 'Keyboard.vala', 'OSK' / 'Window' / 'Keyboard' / 'KeyButton.vala', 'OSK' / 'Window' / 'Keyboard' / 'ViewContainer.vala',