From 8915f9ce1e53e39eec905f09b380ffccdd528b25 Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Sat, 31 Jan 2026 21:05:54 -0500 Subject: [PATCH] input sources: Allow a default xkb layout to be configured for Ibus input methods that support it. By default, the last-used layout (or en-us) is used when switching to a mozc or m17n-based im. This can be overridden at the time of their activation. Add a setting, and repurpose the 'configure' button in the keyboard layout settings to show a dialog that will allow the user to choose a default layout, as well as launch that engine's configuration program if one is available. This will be disabled for engines whose default layout is not 'default' (chewing, hangul, sunpinyin, libthai). Depends on https://github.com/linuxmint/cinnamon-desktop/pull/263 ref: https://forums.linuxmint.com/viewtopic.php?t=461850 --- .../bin/AddKeyboardLayout.py | 22 +- .../cinnamon-settings/bin/InputSources.py | 176 +++++++++++++++- .../bin/input-sources-list.ui | 199 ++++++++++++++++++ js/ui/keyboardManager.js | 48 ++++- 4 files changed, 421 insertions(+), 24 deletions(-) diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/AddKeyboardLayout.py b/files/usr/share/cinnamon/cinnamon-settings/bin/AddKeyboardLayout.py index baca3656f9..85f38f0067 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/bin/AddKeyboardLayout.py +++ b/files/usr/share/cinnamon/cinnamon-settings/bin/AddKeyboardLayout.py @@ -44,9 +44,10 @@ def make_ibus_display_name(engine): LAYOUT_VARIANT_COLUMN = 4 class AddKeyboardLayoutDialog(): - def __init__(self, used_ids): + def __init__(self, used_ids, xkb_only=False): self.input_source_settings = Gio.Settings(schema_id=INPUT_SOURCE_SETTINGS) self.original_used_ids = set(used_ids) + self.xkb_only = xkb_only builder = Gtk.Builder() builder.set_translation_domain('cinnamon') @@ -84,12 +85,17 @@ def __init__(self, used_ids): column.pack_start(cell, True) column.add_attribute(cell, "text", LAYOUT_DISPLAY_NAME_COLUMN) - column = Gtk.TreeViewColumn(title=_("Input method")) - column.set_sort_column_id(LAYOUT_TYPE_COLUMN) - self.layouts_view.append_column(column) + self.ibus_column = Gtk.TreeViewColumn(title=_("Input method")) + self.ibus_column.set_sort_column_id(LAYOUT_TYPE_COLUMN) + self.layouts_view.append_column(self.ibus_column) cell = Gtk.CellRendererText(xpad=10) - column.pack_start(cell, False) - column.set_cell_data_func(cell, self.layout_type_data_func) + self.ibus_column.pack_start(cell, False) + self.ibus_column.set_cell_data_func(cell, self.layout_type_data_func) + + # Hide IBus column and change title when in XKB-only mode + if self.xkb_only: + self.ibus_column.set_visible(False) + self.dialog.set_title(_("Choose a Layout")) self.response_id = None @@ -124,6 +130,10 @@ def _on_ibus_connected(self, ibus, data=None): ibus.list_engines_async(5000, None, self._list_ibus_engines_completed) def _list_ibus_engines_completed(self, ibus, res, data=None): + # Skip IBus engines in XKB-only mode + if self.xkb_only: + return + try: engines = ibus.list_engines_async_finish(res) except GLib.Error as e: diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/InputSources.py b/files/usr/share/cinnamon/cinnamon-settings/bin/InputSources.py index 76dc502fb8..e8a4caf00e 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/bin/InputSources.py +++ b/files/usr/share/cinnamon/cinnamon-settings/bin/InputSources.py @@ -132,10 +132,7 @@ def on_model_updated(self, model, position, removed, added, data=None): def _update_selected_row(self, data=None): self.input_sources_list.handler_block(self.source_activate_handler) - for row in self.input_sources_list.get_children(): - source = row.get_child().input_source - if source.active: - self.input_sources_list.select_row(row) + self.input_sources_list.unselect_all() self.input_sources_list.handler_unblock(self.source_activate_handler) self.update_widgets() @@ -178,8 +175,9 @@ def on_test_layout_clicked(self, button, data=None): def on_engine_config_clicked(self, button, data=None): source = self._get_selected_source() - - subprocess.Popen([source.preferences], shell=True) + dialog = IBusConfigDialog(source, self.current_input_sources_model.input_source_settings) + dialog.run() + dialog.destroy() def update_widgets(self): # Don't allow removal of last remaining layout @@ -189,7 +187,8 @@ def update_widgets(self): source = self._get_selected_source() if source is not None: self.test_layout_button.set_sensitive(source.type == "xkb") - self.engine_config_button.set_sensitive(source.type == "ibus" and source.preferences != '') + # Enable Configure button for all IBus sources (not just those with preferences) + self.engine_config_button.set_sensitive(source.type == "ibus") index = self.current_input_sources_model.get_item_index(source) self.move_layout_up_button.set_sensitive(index > 0) self.move_layout_down_button.set_sensitive(index < self.current_input_sources_model.get_n_items() - 1) @@ -201,6 +200,142 @@ def update_widgets(self): self.move_layout_down_button.set_sensitive(False) self.remove_layout_button.set_sensitive(False) + +class IBusConfigDialog(): + def __init__(self, source, settings): + self.source = source + self.settings = settings + self.xkb_info = CinnamonDesktop.XkbInfo.new_with_extras() + + # Check if this engine uses "default" layout or has a specific one + self.engine_layout = self._get_engine_layout() + self.allows_override = (self.engine_layout == "default") + + builder = Gtk.Builder() + builder.set_translation_domain('cinnamon') + builder.add_from_file("/usr/share/cinnamon/cinnamon-settings/bin/input-sources-list.ui") + + self.dialog = builder.get_object("ibus_config_dialog") + + # Set up name label + name_label = builder.get_object("ibus_config_name_label") + name_label.set_text(source.display_name) + + # Set up explanation label based on whether override is allowed + explanation_label = builder.get_object("ibus_config_explanation_label") + if not self.allows_override: + layout_display = self._get_engine_layout_display_name() + explanation_label.set_text( + _("This input method requires the \"%s\" keyboard layout to function correctly. " + "The layout is set automatically when you switch to this input method.") % layout_display + ) + + # Set up layout label + self.layout_label = builder.get_object("ibus_config_layout_label") + + # Set up buttons + close_button = builder.get_object("ibus_config_close_button") + close_button.connect("clicked", self.on_close_clicked) + + self.change_layout_button = builder.get_object("ibus_config_change_layout_button") + self.change_layout_button.connect("clicked", self.on_change_layout_clicked) + self.change_layout_button.set_sensitive(self.allows_override) + + self.clear_override_button = builder.get_object("ibus_config_clear_override_button") + self.clear_override_button.connect("clicked", self.on_clear_override_clicked) + + engine_settings_button = builder.get_object("ibus_config_engine_settings_button") + if source.preferences: + engine_settings_button.connect("clicked", self.on_engine_settings_clicked) + else: + engine_settings_button.set_visible(False) + + self.update_layout_display() + self.dialog.show_all() + + def _get_engine_layout(self): + ibus = IBus.Bus.new() + if ibus.is_connected(): + engines = ibus.get_engines_by_names([self.source.id]) + if engines: + return engines[0].get_layout() or "default" + return "default" + + def _get_engine_layout_display_name(self): + if not self.engine_layout or self.engine_layout == "default": + return _("Default") + got, display_name, short_name, layout, variant = self.xkb_info.get_layout_info(self.engine_layout) + if got: + return f"{display_name} ({self.engine_layout})" + return self.engine_layout + + def run(self): + return self.dialog.run() + + def destroy(self): + self.dialog.destroy() + + def on_close_clicked(self, button, data=None): + self.dialog.response(Gtk.ResponseType.CLOSE) + + def get_current_override(self): + source_layouts = self.settings.get_value("source-layouts").unpack() + return source_layouts.get(self.source.id, None) + + def get_layout_display_name(self, layout_id): + if layout_id is None: + return None + got, display_name, short_name, layout, variant = self.xkb_info.get_layout_info(layout_id) + if got: + return f"{display_name} ({layout_id})" + return layout_id + + def update_layout_display(self): + if not self.allows_override: + # Engine has a fixed layout requirement + self.layout_label.set_text(self._get_engine_layout_display_name()) + self.clear_override_button.set_sensitive(False) + return + + override = self.get_current_override() + if override: + display_name = self.get_layout_display_name(override) + self.layout_label.set_text(display_name) + self.clear_override_button.set_sensitive(True) + else: + self.layout_label.set_text(_("Default")) + self.clear_override_button.set_sensitive(False) + + def on_change_layout_clicked(self, button, data=None): + # Show the layout picker in XKB-only mode + add_dialog = AddKeyboardLayout.AddKeyboardLayoutDialog([], xkb_only=True) + add_dialog.dialog.set_transient_for(self.dialog) + add_dialog.dialog.show_all() + ret = add_dialog.dialog.run() + if ret == Gtk.ResponseType.OK: + layout_type, layout_id = add_dialog.response + self.set_layout_override(layout_id) + add_dialog.dialog.destroy() + + def on_clear_override_clicked(self, button, data=None): + self.clear_layout_override() + + def on_engine_settings_clicked(self, button, data=None): + subprocess.Popen([self.source.preferences], shell=True) + + def set_layout_override(self, layout_id): + source_layouts = self.settings.get_value("source-layouts").unpack() + source_layouts[self.source.id] = layout_id + self.settings.set_value("source-layouts", GLib.Variant("a{ss}", source_layouts)) + self.update_layout_display() + + def clear_layout_override(self): + source_layouts = self.settings.get_value("source-layouts").unpack() + if self.source.id in source_layouts: + del source_layouts[self.source.id] + self.settings.set_value("source-layouts", GLib.Variant("a{ss}", source_layouts)) + self.update_layout_display() + class LayoutIcon(Gtk.Overlay): def __init__(self, file, dupe_id): Gtk.Overlay.__init__(self) @@ -267,6 +402,7 @@ def __init__(self): self.interface_settings = Gio.Settings(schema_id="org.cinnamon.desktop.interface") self.interface_settings.connect("changed", self.on_interface_settings_changed) self.input_source_settings = Gio.Settings(schema_id="org.cinnamon.desktop.input-sources") + self.input_source_settings.connect("changed::source-layouts", self.on_source_layouts_changed) try: Gio.DBusProxy.new_for_bus(Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None, @@ -293,6 +429,16 @@ def live(self): return False return True + def _get_layout_override(self, engine_id): + source_layouts = self.input_source_settings.get_value("source-layouts").unpack() + return source_layouts.get(engine_id, None) + + def _get_layout_display_name(self, layout_id): + got, display_name, short_name, layout, variant = self.xkb_info.get_layout_info(layout_id) + if got: + return display_name + return layout_id + def _on_ibus_connected(self, ibus, data=None): if self._proxy is None: self.refresh_input_source_list() @@ -324,6 +470,9 @@ def on_interface_settings_changed(self, settings, key, data=None): if key.startswith("keyboard-layout-"): self.refresh_input_source_list() + def on_source_layouts_changed(self, settings, key, data=None): + self.refresh_input_source_list() + def refresh_input_source_list(self): if self.live: layouts = self._proxy.GetInputSources() @@ -377,7 +526,18 @@ def show_add_layout_dialog(self): def create_row(self, source, data=None): row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) - markup = f"{source.display_name}" + + # Build display name, including layout override for IBus sources + display_name = GLib.markup_escape_text(source.display_name) + if source.type == "ibus": + layout_override = self._get_layout_override(source.id) + if layout_override: + layout_display = GLib.markup_escape_text(self._get_layout_display_name(layout_override)) + markup = f"{display_name} | {layout_display}" + else: + markup = f"{display_name}" + else: + markup = f"{display_name}" label = Gtk.Label(label=markup, xalign=0.0, use_markup=True, margin_start=4) row.pack_start(label, True, True, 0) diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/input-sources-list.ui b/files/usr/share/cinnamon/cinnamon-settings/bin/input-sources-list.ui index c3bbfb49e8..3d883ff966 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/bin/input-sources-list.ui +++ b/files/usr/share/cinnamon/cinnamon-settings/bin/input-sources-list.ui @@ -142,6 +142,205 @@ + + False + 12 + Configure Input Method + 450 + dialog + + + False + vertical + 12 + + + False + + + Close + True + True + True + + + True + True + 0 + + + + + False + False + 3 + + + + + True + False + center + 0 + + + + + + + False + True + 0 + + + + + True + False + 0 + + + True + False + 12 + 12 + 12 + 12 + vertical + 12 + + + True + False + By default, this input method uses whichever keyboard layout was active before switching to it. You can override this to always use a specific layout. + True + 50 + 0 + + + + False + True + 0 + + + + + True + False + 12 + + + True + False + Layout: + 0 + + + False + True + 0 + + + + + True + False + True + end + 0 + + + + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 6 + + + Change + True + True + True + + + False + True + 0 + + + + + Reset + True + True + True + + + False + True + 1 + + + + + False + True + 2 + + + + + + + True + False + Keyboard Layout + + + + + + + + False + True + 1 + + + + + Engine Settings + True + True + True + + + False + False + 2 + + + + + True False diff --git a/js/ui/keyboardManager.js b/js/ui/keyboardManager.js index c4ed55a3ae..7c4d879ede 100644 --- a/js/ui/keyboardManager.js +++ b/js/ui/keyboardManager.js @@ -16,6 +16,7 @@ DESKTOP_INPUT_SOURCES_SCHEMA = 'org.cinnamon.desktop.input-sources'; KEY_INPUT_SOURCES = 'sources'; KEY_KEYBOARD_OPTIONS = 'xkb-options'; KEY_PER_WINDOW = 'per-window'; +KEY_SOURCE_LAYOUTS = 'source-layouts'; var INPUT_SOURCE_TYPE_XKB = 'xkb'; var INPUT_SOURCE_TYPE_IBUS = 'ibus'; @@ -176,7 +177,7 @@ var KeyboardManager = class { }; var InputSource = class { - constructor(type, id, displayName, shortName, flagName, xkbLayout, variant, prefs, index) { + constructor(type, id, displayName, shortName, flagName, xkbLayout, variant, prefs, index, layoutOverride) { this.type = type; this.id = id; this.displayName = displayName; @@ -187,6 +188,7 @@ var InputSource = class { this.xkbLayout = xkbLayout; this.variant = variant; this.preferences = prefs; + this._layoutOverride = layoutOverride; this.properties = null; @@ -207,6 +209,10 @@ var InputSource = class { } _getXkbId() { + // Use layout override if set + if (this._layoutOverride) + return this._layoutOverride; + let engineDesc = IBusManager.getIBusManager().getEngineDesc(this.id); if (!engineDesc) return this.id; @@ -411,6 +417,7 @@ var InputSourceSettings = class { this._settings.connect('changed::%s'.format(KEY_INPUT_SOURCES), this._emitInputSourcesChanged.bind(this)); this._settings.connect('changed::%s'.format(KEY_KEYBOARD_OPTIONS), this._emitKeyboardOptionsChanged.bind(this)); this._settings.connect('changed::%s'.format(KEY_PER_WINDOW), this._emitPerWindowChanged.bind(this)); + this._settings.connect('changed::%s'.format(KEY_SOURCE_LAYOUTS), this._emitInputSourcesChanged.bind(this)); let sources = this._settings.get_value(KEY_INPUT_SOURCES); if (sources.n_children() == 0) { @@ -463,6 +470,10 @@ var InputSourceSettings = class { get perWindow() { return this._settings.get_boolean(KEY_PER_WINDOW); } + + get sourceLayouts() { + return this._settings.get_value(KEY_SOURCE_LAYOUTS).deep_unpack(); + } }; Signals.addSignalMethods(InputSourceSettings.prototype); @@ -731,17 +742,34 @@ var InputSourceManager = class { } let inputSourcesDupeTracker = {}; + let sourceLayouts = this._settings.sourceLayouts; for (let i = 0; i < infosList.length; i++) { - let is = new InputSource(infosList[i].type, - infosList[i].id, - infosList[i].displayName, - infosList[i].shortName, - infosList[i].flagName, - infosList[i].xkbLayout, - infosList[i].variant, - infosList[i].prefs, - i); + let info = infosList[i]; + let layoutOverride = null; + + // Only apply layout overrides for IBus engines that use "default" layout + if (info.type == INPUT_SOURCE_TYPE_IBUS && info.id in sourceLayouts) { + let engineDesc = this._ibusManager.getEngineDesc(info.id); + let engineLayout = engineDesc ? engineDesc.get_layout() : null; + + if (!engineLayout || engineLayout == 'default') { + layoutOverride = sourceLayouts[info.id]; + } else { + global.logWarning('Ignoring layout override for IBus engine "%s": engine requires layout "%s"'.format(info.id, engineLayout)); + } + } + + let is = new InputSource(info.type, + info.id, + info.displayName, + info.shortName, + info.flagName, + info.xkbLayout, + info.variant, + info.prefs, + i, + layoutOverride); is.connect('activate', this.activateInputSource.bind(this)); let key = is.shortName;