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;