Adapt to keyboard layout API changes

Resolves: RHEL-106779
This commit is contained in:
Jonas Ådahl 2026-05-20 21:05:54 +02:00
parent e8e9624acd
commit 3f21ead5da
3 changed files with 462 additions and 0 deletions

View File

@ -0,0 +1,30 @@
From 0aa8ffb402fab4b0841540f271ab0401b1185907 Mon Sep 17 00:00:00 2001
From: Carlos Garnacho <carlosg@gnome.org>
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: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/4068>
(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

View File

@ -0,0 +1,428 @@
From 36ba4b88b3739b4862022f62aeddc9ae8c757bd8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jonas=20=C3=85dahl?= <jadahl@gmail.com>
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: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3901>
(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

View File

@ -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