diff --git a/0001-environment-Drop-Meta.Backend.set_keymap_layout_grou.patch b/0001-environment-Drop-Meta.Backend.set_keymap_layout_grou.patch new file mode 100644 index 0000000..15e560e --- /dev/null +++ b/0001-environment-Drop-Meta.Backend.set_keymap_layout_grou.patch @@ -0,0 +1,30 @@ +From 0aa8ffb402fab4b0841540f271ab0401b1185907 Mon Sep 17 00:00:00 2001 +From: Carlos Garnacho +Date: Thu, 5 Feb 2026 22:56:47 +0100 +Subject: [PATCH] environment: Drop Meta.Backend.set_keymap_layout_group_async + promisify + +This method was removed in mutter, its async promisification is not +necessary anymore. + +Part-of: +(cherry picked from commit ae7917c904dd9d80982e6e353b4bddc7420a4a98) +--- + js/ui/environment.js | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/js/ui/environment.js b/js/ui/environment.js +index f2dc69eef7..3c9d117f17 100644 +--- a/js/ui/environment.js ++++ b/js/ui/environment.js +@@ -33,7 +33,6 @@ Gio._promisify(Gio.File.prototype, 'query_info_async'); + Gio._promisify(Polkit.Permission, 'new'); + Gio._promisify(Shell.App.prototype, 'activate_action'); + Gio._promisify(Meta.Backend.prototype, 'set_keymap_async'); +-Gio._promisify(Meta.Backend.prototype, 'set_keymap_layout_group_async'); + + // We can't import shell JS modules yet, because they may have + // variable initializations, etc, that depend on this file's +-- +2.54.0 + diff --git a/0001-status-keyboard-Adapt-to-external-source-of-keyboard.patch b/0001-status-keyboard-Adapt-to-external-source-of-keyboard.patch new file mode 100644 index 0000000..179eb2e --- /dev/null +++ b/0001-status-keyboard-Adapt-to-external-source-of-keyboard.patch @@ -0,0 +1,428 @@ +From 36ba4b88b3739b4862022f62aeddc9ae8c757bd8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Jonas=20=C3=85dahl?= +Date: Fri, 3 Oct 2025 17:02:37 +0200 +Subject: [PATCH] status/keyboard: Adapt to external source of keyboard layout + +This adapts to API changes that allow an external source (e.g. remote +desktop API user) to control the current keyboard layout. This is done +in two ways: + +If the external keyboard source sets the keymap as "locked", the +indicator is fixed to the short name of the external source, and will +change the indicator label when the layout index is changed accordingly. +The builtin input sources are not selectable, and the keyboard selection +keybinding is disabled. + +If the external keyboard source doesn't set the keymap as "locked", +whenever the current keymap is from an external source, it's indicated +as a non-sensitive but selected input source. Selecting another keyboard +layout overrides the one selected by the external entity. + +Part-of: +(cherry picked from commit 0f90346872477d91a3e0be605a55bca1d163d735) +--- + js/misc/keyboardManager.js | 122 ++++++++++++++++++++++++++------- + js/ui/status/keyboard.js | 135 +++++++++++++++++++++++++++++-------- + 2 files changed, 206 insertions(+), 51 deletions(-) + +diff --git a/js/misc/keyboardManager.js b/js/misc/keyboardManager.js +index 173d2a7c13..4185c61667 100644 +--- a/js/misc/keyboardManager.js ++++ b/js/misc/keyboardManager.js +@@ -1,5 +1,8 @@ + import GLib from 'gi://GLib'; + import GnomeDesktop from 'gi://GnomeDesktop'; ++import Meta from 'gi://Meta'; ++ ++import * as Signals from './signals.js'; + + import * as Main from '../ui/main.js'; + +@@ -40,8 +43,10 @@ export function holdKeyboard() { + global.backend.freeze_keyboard(global.get_current_time()); + } + +-class KeyboardManager { ++class KeyboardManager extends Signals.EventEmitter { + constructor() { ++ super(); ++ + // The XKB protocol doesn't allow for more than 4 layouts in a + // keymap. Wayland doesn't impose this limit and libxkbcommon can + // handle up to 32 layouts but since we need to support X clients +@@ -53,48 +58,87 @@ class KeyboardManager { + this._localeLayoutInfo = this._getLocaleLayout(); + this._layoutInfos = {}; + this._currentKeymap = null; ++ ++ global.backend.connect('keymap-changed', this._onKeymapChanged.bind(this)); ++ global.backend.connect('keymap-layout-group-changed', this._onKeymapLayoutGroupChanged.bind(this)); ++ global.backend.connect('reset-keymap-description', ++ () => this._ourKeymapDescription); ++ global.backend.connect('reset-keymap-layout-index', ++ () => this._current.groupIndex); + } + +- async _applyLayoutGroup(group) { +- let options = this._buildOptionsString(); +- let [layouts, variants] = this._buildGroupStrings(group); +- let model = this._xkbModel; ++ _updateCurrentKeymap(info) { ++ const options = this._buildOptionsString(); ++ const [layouts, variants] = this._buildGroupStrings(info.group); ++ const model = this._xkbModel; + + if (this._currentKeymap && + this._currentKeymap.layouts === layouts && + this._currentKeymap.variants === variants && + this._currentKeymap.options === options && + this._currentKeymap.model === model) +- return; ++ return false; ++ ++ const displayNames = info.group.map(g => g.displayName); ++ const shortNames = info.group.map(g => g.shortName); ++ this._currentKeymap = { ++ layouts, ++ variants, ++ options, ++ model, ++ displayNames, ++ shortNames, ++ }; ++ return true; ++ } + +- this._currentKeymap = {layouts, variants, options, model}; +- await global.backend.set_keymap_async(layouts, variants, options, model, null); ++ _createKeymapDescription() { ++ return Meta.KeymapDescription.new_from_rules(this._currentKeymap.model, ++ this._currentKeymap.layouts, ++ this._currentKeymap.variants, ++ this._currentKeymap.options, ++ this._currentKeymap.displayNames, ++ this._currentKeymap.shortNames ++ ); + } + +- async _applyLayoutGroupIndex(idx) { +- await global.backend.set_keymap_layout_group_async(idx, null); ++ _onKeymapChanged() { ++ this._keymapDescription = global.backend.get_keymap_description(); ++ this.emit('keymap-changed'); + } + +- async _doApply(info) { +- await this._applyLayoutGroup(info.group); +- await this._applyLayoutGroupIndex(info.groupIndex); ++ _onKeymapLayoutGroupChanged() { ++ this.emit('keymap-changed'); + } + +- apply(id) { +- let info = this._layoutInfos[id]; ++ async _doApply(id) { ++ const info = this._layoutInfos[id]; + if (!info) + return; + +- if (this._current && this._current.group === info.group) { +- if (this._current.groupIndex !== info.groupIndex) +- this._applyLayoutGroupIndex(info.groupIndex).catch(logError); +- } else { +- this._doApply(info).catch(logError); ++ let recreate; ++ if (this._updateCurrentKeymap(info)) ++ recreate = true; ++ else if (this.isExternal()) ++ recreate = true; ++ else ++ recreate = false; ++ ++ if (recreate) ++ this._ourKeymapDescription = this._createKeymapDescription(); ++ ++ if (recreate || !this._current || this._current.groupIndex !== info.groupIndex) { ++ await global.backend.set_keymap_async( ++ this._ourKeymapDescription, info.groupIndex, null); + } + + this._current = info; + } + ++ apply(id) { ++ this._doApply(id).catch(logError); ++ } ++ + reapply() { + if (!this._current) + return; +@@ -107,9 +151,17 @@ class KeyboardManager { + this._layoutInfos = {}; + + for (const id of ids) { +- let [found, , , layout, variant] = this._xkbInfo.get_layout_info(id); +- if (found) +- this._layoutInfos[id] = {id, layout, variant}; ++ const [found, displayName, shortName, layout, variant] = ++ this._xkbInfo.get_layout_info(id); ++ if (found) { ++ this._layoutInfos[id] = { ++ id, ++ layout, ++ variant, ++ displayName, ++ shortName, ++ }; ++ } + } + + let i = 0; +@@ -173,4 +225,28 @@ class KeyboardManager { + get currentLayout() { + return this._current; + } ++ ++ get shortName() { ++ const seat = global.stage.context.get_backend().get_default_seat(); ++ const keymap = seat.get_keymap(); ++ return keymap.get_current_short_name(); ++ } ++ ++ get displayName() { ++ const seat = global.stage.context.get_backend().get_default_seat(); ++ const keymap = seat.get_keymap(); ++ return keymap.get_current_display_name(); ++ } ++ ++ isLocked() { ++ return this._keymapDescription?.is_locked() ?? false; ++ } ++ ++ isExternal() { ++ if (!this._keymapDescription) ++ return false; ++ if (!this._ourKeymapDescription) ++ return true; ++ return !this._keymapDescription.direct_equal(this._ourKeymapDescription); ++ } + } +diff --git a/js/ui/status/keyboard.js b/js/ui/status/keyboard.js +index 9245dbeed4..d9a8b9d6dc 100644 +--- a/js/ui/status/keyboard.js ++++ b/js/ui/status/keyboard.js +@@ -25,8 +25,6 @@ class LayoutMenuItem extends PopupMenu.PopupBaseMenuItem { + _init(displayName, shortName) { + super._init(); + +- this.setOrnament(PopupMenu.Ornament.NO_DOT); +- + this.label = new St.Label({ + text: displayName, + x_expand: true, +@@ -371,6 +369,7 @@ export class InputSourceManager extends Signals.EventEmitter { + + this._xkbInfo = KeyboardManager.getXkbInfo(); + this._keyboardManager = KeyboardManager.getKeyboardManager(); ++ this._keyboardManager.connect('keymap-changed', this._keymapChanged.bind(this)); + + this._ibusReady = false; + this._ibusManager = IBusManager.getIBusManager(); +@@ -429,6 +428,9 @@ export class InputSourceManager extends Signals.EventEmitter { + } + + _switchInputSource(display, window, event, binding) { ++ if (this._keyboardManager.isLocked()) ++ return; ++ + if (this._mruSources.length < 2) + return; + +@@ -462,6 +464,10 @@ export class InputSourceManager extends Signals.EventEmitter { + this._keyboardManager.reapply(); + } + ++ _keymapChanged() { ++ this.emit('keymap-changed'); ++ } ++ + _updateMruSettings() { + // If IBus is not ready we don't have a full picture of all + // the available sources, so don't update the setting +@@ -912,7 +918,8 @@ class InputSourceIndicator extends PanelMenu.Button { + this._inputSourceManager = getInputSourceManager(); + this._inputSourceManager.connectObject( + 'sources-changed', this._sourcesChanged.bind(this), +- 'current-source-changed', this._currentSourceChanged.bind(this), this); ++ 'current-source-changed', this._currentSourceChanged.bind(this), ++ 'keymap-changed', this._keymapChanged.bind(this), this); + this._inputSourceManager.reload(); + } + +@@ -928,47 +935,114 @@ class InputSourceIndicator extends PanelMenu.Button { + this._showLayoutItem.visible = Main.sessionMode.allowSettings; + } + +- _sourcesChanged() { +- for (let i in this._menuItems) +- this._menuItems[i].destroy(); +- for (let i in this._indicatorLabels) +- this._indicatorLabels[i].destroy(); ++ _createExternalSource(keyboardManager) { ++ const {displayName, shortName} = keyboardManager; ++ const is = new InputSource('external', 'external', displayName, shortName, -1); ++ is.locked = keyboardManager.isLocked(); ++ return is; ++ } + +- this._menuItems = {}; +- this._indicatorLabels = {}; ++ _getCurrentSource() { ++ const {keyboardManager} = this._inputSourceManager; ++ if (keyboardManager.isExternal()) ++ return this._inputSources[0]; ++ else ++ return this._inputSourceManager.currentSource; ++ } ++ ++ _updateInputSources() { ++ const {keyboardManager} = this._inputSourceManager; ++ if (keyboardManager.isLocked()) { ++ const is = this._createExternalSource(keyboardManager); ++ this._inputSources = [is]; ++ return; ++ } ++ ++ const inputSources = []; ++ if (keyboardManager.isExternal()) ++ inputSources.push(this._createExternalSource(keyboardManager)); ++ ++ const internalSources = this._inputSourceManager.inputSources; ++ inputSources.push( ++ ...Object.keys(internalSources).sort((a, b) => a - b).map(i => internalSources[i])); ++ ++ this._inputSources = inputSources; ++ } ++ ++ _addSourceIndicators() { ++ const inputSources = this._inputSources; ++ for (const i in inputSources) { ++ const is = inputSources[i]; + +- let menuIndex = 0; +- for (let i in this._inputSourceManager.inputSources) { +- let is = this._inputSourceManager.inputSources[i]; ++ const menuItem = new LayoutMenuItem(is.displayName, is.shortName); ++ if (is.type !== 'external') ++ menuItem.connect('activate', () => is.activate(true)); ++ else ++ menuItem.sensitive = false; + +- let menuItem = new LayoutMenuItem(is.displayName, is.shortName); +- menuItem.connect('activate', () => is.activate(true)); ++ if (is.locked) ++ menuItem.setOrnament(PopupMenu.Ornament.HIDDEN); ++ else ++ menuItem.setOrnament(PopupMenu.Ornament.NO_DOT); + + const indicatorLabel = new St.Label({ + text: is.shortName, + visible: false, + }); + +- this._menuItems[i] = menuItem; +- this._indicatorLabels[i] = indicatorLabel; ++ const indicatorIndex = this._calculateSourceIndicatorIndex(is); ++ ++ this._menuItems[indicatorIndex] = menuItem; ++ this._indicatorLabels[indicatorIndex] = indicatorLabel; + is.connect('changed', () => { + menuItem.indicator.set_text(is.shortName); + indicatorLabel.set_text(is.shortName); + }); + +- this.menu.addMenuItem(menuItem, menuIndex++); ++ this.menu.addMenuItem(menuItem, indicatorIndex); + this._container.add_child(indicatorLabel); + } + } + ++ _sourcesChanged() { ++ this._updateInputSources(); ++ ++ for (const i in this._menuItems) ++ this._menuItems[i].destroy(); ++ for (let i in this._indicatorLabels) ++ this._indicatorLabels[i].destroy(); ++ ++ this._menuItems = {}; ++ this._indicatorLabels = {}; ++ ++ this._addSourceIndicators(); ++ } ++ ++ _calculateSourceIndicatorIndex(source) { ++ const keyboardManager = this._inputSourceManager.keyboardManager; ++ return (keyboardManager.isExternal() ? 1 : 0) + source.index; ++ } ++ ++ _setSourceAsActive(source) { ++ const index = this._calculateSourceIndicatorIndex(source); ++ if (!source.locked) ++ this._menuItems[index]?.setOrnament(PopupMenu.Ornament.DOT); ++ this._indicatorLabels[index]?.show(); ++ } ++ ++ _setSourceAsInactive(source) { ++ const index = this._calculateSourceIndicatorIndex(source); ++ if (!source.locked) ++ this._menuItems[index]?.setOrnament(PopupMenu.Ornament.NO_DOT); ++ this._indicatorLabels[index].hide(); ++ } ++ + _currentSourceChanged(manager, oldSource) { +- let nVisibleSources = Object.keys(this._inputSourceManager.inputSources).length; +- let newSource = this._inputSourceManager.currentSource; ++ const nVisibleSources = Object.keys(this._inputSources).length; ++ const newSource = this._getCurrentSource(); + +- if (oldSource) { +- this._menuItems[oldSource.index].setOrnament(PopupMenu.Ornament.NO_DOT); +- this._indicatorLabels[oldSource.index].hide(); +- } ++ if (oldSource) ++ this._setSourceAsInactive(oldSource); + + if (!newSource || (nVisibleSources < 2 && !newSource.properties)) { + // This source index might be invalid if we weren't able +@@ -986,8 +1060,12 @@ class InputSourceIndicator extends PanelMenu.Button { + + this._buildPropSection(newSource.properties); + +- this._menuItems[newSource.index].setOrnament(PopupMenu.Ornament.DOT); +- this._indicatorLabels[newSource.index].show(); ++ this._setSourceAsActive(newSource); ++ } ++ ++ _keymapChanged() { ++ this._sourcesChanged(); ++ this._setSourceAsActive(this._getCurrentSource()); + } + + _buildPropSection(properties) { +@@ -1023,9 +1101,10 @@ class InputSourceIndicator extends PanelMenu.Button { + else + text = prop.get_label().get_text(); + +- let currentSource = this._inputSourceManager.currentSource; ++ const currentSource = this._getCurrentSource(); + if (currentSource) { +- let indicatorLabel = this._indicatorLabels[currentSource.index]; ++ const index = this._calculateSourceIndicatorIndex(currentSource); ++ const indicatorLabel = this._indicatorLabels[index]; + if (text && text.length > 0 && text.length < 3) + indicatorLabel.set_text(text); + } +-- +2.54.0 + diff --git a/gnome-shell.spec b/gnome-shell.spec index aa74329..8da6f2d 100644 --- a/gnome-shell.spec +++ b/gnome-shell.spec @@ -61,6 +61,10 @@ Patch: screenshot-tool.patch Patch: 0001-Revert-status-keyboard-Limit-the-input-method-indica.patch Patch: 0001-Revert-Require-gjs-1.81.2-for-build-because-Intl.Seg.patch +# Adapt to keyboard layout API changes (RHEL-106779) +Patch: 0001-status-keyboard-Adapt-to-external-source-of-keyboard.patch +Patch: 0001-environment-Drop-Meta.Backend.set_keymap_layout_grou.patch + %define eds_version 3.45.1 %define gnome_desktop_version 44.0-7 %define glib2_version 2.79.2