From 7e045257ee63b932cda786b112ab806d0ff5220a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=E1=BB=B3nh=20Thi=E1=BB=87n=20L=E1=BB=99c?= Date: Fri, 30 Oct 2026 23:50:44 +0700 Subject: [PATCH] feat(osd): add keyboard layout OSD (#4034) port 2.0 (cherry picked from commit 16fc39cc160ebe88cbea520c1f45b9ea8d678d0d) --- quickshell/Common/SettingsData.qml | 1 + quickshell/Common/settings/SettingsSpec.js | 3 + quickshell/DMSShell.qml | 6 ++ .../DBar/Widgets/KeyboardLayoutLabels.js | 2 + .../DBar/Widgets/KeyboardLayoutName.qml | 2 +- quickshell/Modules/OSD/KeyboardLayoutOSD.qml | 87 +++++++++++++++++++ quickshell/Modules/Settings/OSDTab.qml | 13 +++ .../tests/unit/tst_KeyboardLayoutLabels.qml | 2 +- .../translations/settings_search_index.json | 15 ++++ 9 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 quickshell/Modules/OSD/KeyboardLayoutOSD.qml diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 0197850c7..ec5016568 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -1132,6 +1132,7 @@ Singleton { property bool osdMicMuteEnabled: Spec.SPEC.osdMicMuteEnabled.def property bool osdMicVolumeEnabled: Spec.SPEC.osdMicVolumeEnabled.def property bool osdCapsLockEnabled: Spec.SPEC.osdCapsLockEnabled.def + property bool osdKeyboardLayoutEnabled: Spec.SPEC.osdKeyboardLayoutEnabled.def property bool osdTouchpadEnabled: Spec.SPEC.osdTouchpadEnabled.def property bool osdPowerProfileEnabled: Spec.SPEC.osdPowerProfileEnabled.def property bool osdAudioOutputEnabled: Spec.SPEC.osdAudioOutputEnabled.def diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index dd5b1ab0d..5a8d0ab77 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -1478,6 +1478,9 @@ var LOCAL_SPEC = { osdCapsLockEnabled: { def: true }, + osdKeyboardLayoutEnabled: { + def: false + }, osdTouchpadEnabled: { def: true }, diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index 5878fd65f..fcb112ba6 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -1315,6 +1315,12 @@ Item { delegate: CapsLockOSD {} } + Variants { + model: (SettingsData.osdKeyboardLayoutEnabled && KeyboardLayoutService.available) ? SettingsData.getFilteredScreens("osd") : [] + + delegate: KeyboardLayoutOSD {} + } + Variants { model: (SettingsData.osdTouchpadEnabled && CompositorService.isNiri) ? root.legacySystemLevelOsdScreens : [] diff --git a/quickshell/Modules/DBar/Widgets/KeyboardLayoutLabels.js b/quickshell/Modules/DBar/Widgets/KeyboardLayoutLabels.js index 105bb4e17..138b93b69 100644 --- a/quickshell/Modules/DBar/Widgets/KeyboardLayoutLabels.js +++ b/quickshell/Modules/DBar/Widgets/KeyboardLayoutLabels.js @@ -1,6 +1,8 @@ .pragma library .import "../../../DCommon/Common/LayoutCodes.js" as LayoutCodes +var VALID_VARIANTS = ["US", "UK", "GB", "AZERTY", "QWERTY", "Dvorak", "Colemak", "Mac", "Intl", "International"]; + // Overrides are keyed by whatever string would otherwise be displayed, so the same // map applies after any compositor-specific shortening (compact vs. vertical mode). function applyOverride(label, overrides) { diff --git a/quickshell/Modules/DBar/Widgets/KeyboardLayoutName.qml b/quickshell/Modules/DBar/Widgets/KeyboardLayoutName.qml index c22728ea6..b47642bae 100644 --- a/quickshell/Modules/DBar/Widgets/KeyboardLayoutName.qml +++ b/quickshell/Modules/DBar/Widgets/KeyboardLayoutName.qml @@ -12,7 +12,7 @@ BasePill { property bool compactMode: SettingsData.widgetOption("keyboard_layout_name", widgetData, "keyboardLayoutNameCompactMode") property bool showIcon: SettingsData.widgetOption("keyboard_layout_name", widgetData, "keyboardLayoutNameShowIcon") readonly property var labelOverrides: SettingsData.widgetOption("keyboard_layout_name", widgetData, "keyboardLayoutNameLabelOverrides") ?? ({}) - readonly property var validVariants: ["US", "UK", "GB", "AZERTY", "QWERTY", "Dvorak", "Colemak", "Mac", "Intl", "International"] + readonly property var validVariants: KeyboardLayoutLabels.VALID_VARIANTS readonly property bool codesOnly: KeyboardLayoutService.namesAreXkbCodes readonly property string currentLayout: compactMode ? KeyboardLayoutService.compactLayout : KeyboardLayoutService.currentLayout readonly property var _allLayoutLabels: (compactMode || !codesOnly ? KeyboardLayoutService.layoutNames : []).map(n => displayLabel(n)) diff --git a/quickshell/Modules/OSD/KeyboardLayoutOSD.qml b/quickshell/Modules/OSD/KeyboardLayoutOSD.qml new file mode 100644 index 000000000..b36650951 --- /dev/null +++ b/quickshell/Modules/OSD/KeyboardLayoutOSD.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Layouts +import qs.Common +import qs.Services +import qs.DCommon.Widgets +import qs.Widgets + +DOSD { + id: root + + osdKind: "keyboardLayout" + + property string lastLayout: "" + property bool layoutInitialized: false + readonly property string currentLayout: KeyboardLayoutService.currentLayout + readonly property string layoutLabel: currentLayout + readonly property real horizontalPadding: Theme.spacingL + readonly property real itemSpacing: Theme.spacingM + readonly property real labelWidth: Math.ceil(labelMetrics.width) + + osdWidth: Math.min(screenWidth - Theme.spacingM * 2, Math.max(180, horizontalPadding * 2 + Theme.buttonHeightS + itemSpacing + labelWidth + Theme.spacingM * 2)) + osdHeight: Theme.osdHeight + autoHideInterval: 2000 + enableMouseInteraction: false + + function observeLayout() { + if (!KeyboardLayoutService.layoutKnown) { + layoutInitialized = false; + lastLayout = ""; + return; + } + if (!layoutInitialized) { + lastLayout = currentLayout; + layoutInitialized = true; + return; + } + if (lastLayout === currentLayout) + return; + lastLayout = currentLayout; + if (SettingsData.osdKeyboardLayoutEnabled) + show(); + } + + onCurrentLayoutChanged: observeLayout() + + Component.onCompleted: { + KeyboardLayoutService.consumers++; + observeLayout(); + } + Component.onDestruction: KeyboardLayoutService.consumers-- + + StyledTextMetrics { + id: labelMetrics + font.pixelSize: Theme.fontSizeMedium + font.weight: Theme.fontWeightMedium + text: root.layoutLabel + } + + content: Item { + anchors.fill: parent + + RowLayout { + anchors { + fill: parent + leftMargin: root.horizontalPadding + rightMargin: root.horizontalPadding + } + spacing: root.itemSpacing + + OsdIcon { + Layout.alignment: Qt.AlignVCenter + iconName: "keyboard" + } + + StyledText { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + text: root.layoutLabel + font.pixelSize: Theme.fontSizeMedium + font.weight: Theme.fontWeightMedium + color: Theme.surfaceText + wrapMode: Text.NoWrap + elide: Text.ElideRight + } + } + } +} diff --git a/quickshell/Modules/Settings/OSDTab.qml b/quickshell/Modules/Settings/OSDTab.qml index e20cd1cbb..9e5825a29 100644 --- a/quickshell/Modules/Settings/OSDTab.qml +++ b/quickshell/Modules/Settings/OSDTab.qml @@ -49,6 +49,11 @@ Item { "label": I18n.tr("Caps lock"), "shown": SettingsData.osdCapsLockEnabled }, + { + "value": "keyboardLayout", + "label": I18n.tr("Keyboard layout"), + "shown": SettingsData.osdKeyboardLayoutEnabled && KeyboardLayoutService.available + }, { "value": "touchpad", "label": I18n.tr("Touchpad"), @@ -190,6 +195,14 @@ Item { onToggled: checked => SettingsData.set("osdCapsLockEnabled", checked) } + SettingsToggleRow { + settingKey: "osdKeyboardLayoutEnabled" + visible: KeyboardLayoutService.available + text: I18n.tr("Keyboard layout") + checked: SettingsData.osdKeyboardLayoutEnabled + onToggled: checked => SettingsData.set("osdKeyboardLayoutEnabled", checked) + } + SettingsToggleRow { settingKey: "osdTouchpadEnabled" visible: CompositorService.isNiri diff --git a/quickshell/tests/unit/tst_KeyboardLayoutLabels.qml b/quickshell/tests/unit/tst_KeyboardLayoutLabels.qml index f7cebc25d..0a3a172bb 100644 --- a/quickshell/tests/unit/tst_KeyboardLayoutLabels.qml +++ b/quickshell/tests/unit/tst_KeyboardLayoutLabels.qml @@ -5,7 +5,7 @@ import "../../Modules/DBar/Widgets/KeyboardLayoutLabels.js" as KeyboardLayoutLab TestCase { name: "KeyboardLayoutLabels" - readonly property var validVariants: ["US", "UK", "GB", "AZERTY", "QWERTY", "Dvorak", "Colemak", "Mac", "Intl", "International"] + readonly property var validVariants: KeyboardLayoutLabels.VALID_VARIANTS function test_overrideReplacesRawHyprlandCompactLabel() { compare(KeyboardLayoutLabels.displayLabel("am-phonetic-alt", true, true, validVariants, { diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index cb36c4463..5f7ad5996 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -8907,6 +8907,21 @@ "timeout" ] }, + { + "section": "osdKeyboardLayoutEnabled", + "label": "Keyboard layout", + "tabIndex": 18, + "category": "On-screen displays", + "keywords": [ + "displays", + "indicator", + "keyboard", + "layout", + "osd", + "popup", + "screen" + ] + }, { "section": "osdMediaPlaybackEnabled", "label": "Media playback",