diff options
| author | AleksElixir <71710534+AleksElixir@users.noreply.github.com> | 2026-01-19 06:51:08 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-19 15:51:08 +1100 |
| commit | 60a567bbba7b52cde5c926b61b9a5d7b3224b2af (patch) | |
| tree | 7b79c3aa4033abcea409b4f09953df0a41b4f1da | |
| parent | controlcenter: add desktopClock configurations (#1097) (diff) | |
| download | caelestia-shell-60a567bbba7b52cde5c926b61b9a5d7b3224b2af.tar.gz caelestia-shell-60a567bbba7b52cde5c926b61b9a5d7b3224b2af.tar.bz2 caelestia-shell-60a567bbba7b52cde5c926b61b9a5d7b3224b2af.zip | |
popouts/kblayout: revamp to be actually functional (#971)
* Replacement KbLayout.qml for keyboard layout management
* Add files via upload
* Update KbLayout source component import
* Remove KbLayout.qml and relocate to kblayout folder
The KbLayout.qml file has been removed and relocated to the kblayout folder.
* Code fixes, and added kbLimit config that toust would be dissableable
* Add 'kbLimit' to configuration options
Added the keyboard limit toast config to the README
* Update KbLayout.qml
* Update KbLayoutModel.qml
* Update BarConfig.qml
* Update Content.qml
* remove old file
---------
Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | config/BarConfig.qml | 1 | ||||
| -rw-r--r-- | config/UtilitiesConfig.qml | 1 | ||||
| -rw-r--r-- | modules/bar/popouts/Content.qml | 7 | ||||
| -rw-r--r-- | modules/bar/popouts/KbLayout.qml | 28 | ||||
| -rw-r--r-- | modules/bar/popouts/kblayout/KbLayout.qml | 158 | ||||
| -rw-r--r-- | modules/bar/popouts/kblayout/KbLayoutModel.qml | 200 |
7 files changed, 367 insertions, 29 deletions
@@ -623,6 +623,7 @@ default, you must create it manually. "dndChanged": true, "gameModeChanged": true, "kbLayoutChanged": true, + "kbLimit": true, "numLockChanged": true, "vpnChanged": true, "nowPlaying": false diff --git a/config/BarConfig.qml b/config/BarConfig.qml index a0ce4be..8226d9e 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -111,5 +111,6 @@ JsonObject { property int trayMenuWidth: 300 property int batteryWidth: 250 property int networkWidth: 320 + property int kbLayoutWidth: 320 } } diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml index 5779d88..cf46446 100644 --- a/config/UtilitiesConfig.qml +++ b/config/UtilitiesConfig.qml @@ -23,6 +23,7 @@ JsonObject { property bool capsLockChanged: true property bool numLockChanged: true property bool kbLayoutChanged: true + property bool kbLimit: true property bool vpnChanged: true property bool nowPlaying: false } diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index da993fa..c9a7c5d 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -6,6 +6,8 @@ import Quickshell import Quickshell.Services.SystemTray import QtQuick +import "./kblayout" + Item { id: root @@ -114,9 +116,12 @@ Item { Popout { name: "kblayout" - sourceComponent: KbLayout {} + sourceComponent: KbLayout { + wrapper: root.wrapper + } } + Popout { name: "lockstatus" sourceComponent: LockStatus {} diff --git a/modules/bar/popouts/KbLayout.qml b/modules/bar/popouts/KbLayout.qml deleted file mode 100644 index ace5af2..0000000 --- a/modules/bar/popouts/KbLayout.qml +++ /dev/null @@ -1,28 +0,0 @@ -import qs.components -import qs.components.controls -import qs.services -import qs.config -import Quickshell -import QtQuick.Layouts - -ColumnLayout { - id: root - - spacing: Appearance.spacing.normal - - StyledText { - Layout.topMargin: Appearance.padding.normal - Layout.rightMargin: Appearance.padding.normal - text: qsTr("Keyboard layout: %1").arg(Hypr.kbLayoutFull) - font.weight: 500 - } - - TextButton { - Layout.bottomMargin: Appearance.padding.normal - Layout.rightMargin: Appearance.padding.normal - Layout.fillWidth: true - - text: qsTr("Switch layout") - onClicked: Hypr.extras.message("switchxkblayout all next") - } -} diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml new file mode 100644 index 0000000..f612f58 --- /dev/null +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -0,0 +1,158 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.components +import qs.components.controls +import qs.services +import qs.config +import qs.utils + +import "." + +ColumnLayout { + id: root + + required property Item wrapper + + spacing: Appearance.spacing.small + width: Config.bar.sizes.kbLayoutWidth + + KbLayoutModel { id: kb } + + function refresh() { kb.refresh() } + Component.onCompleted: kb.start() + + StyledText { + Layout.topMargin: Appearance.padding.normal + Layout.rightMargin: Appearance.padding.small + text: qsTr("Keyboard Layouts") + font.weight: 500 + } + + ListView { + id: list + model: kb.visibleModel + + Layout.fillWidth: true + Layout.rightMargin: Appearance.padding.small + Layout.topMargin: Appearance.spacing.small + + clip: true + interactive: true + implicitHeight: Math.min(contentHeight, 320) + visible: kb.visibleModel.count > 0 + spacing: Appearance.spacing.small + + add: Transition { + NumberAnimation { properties: "opacity"; from: 0; to: 1; duration: 140 } + NumberAnimation { properties: "y"; duration: 180; easing.type: Easing.OutCubic } + } + remove: Transition { NumberAnimation { properties: "opacity"; to: 0; duration: 100 } } + move: Transition { NumberAnimation { properties: "y"; duration: 180; easing.type: Easing.OutCubic } } + displaced: Transition { NumberAnimation { properties: "y"; duration: 180; easing.type: Easing.OutCubic } } + + delegate: Item { + required property int layoutIndex + required property string label + + width: list.width + height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2) + + readonly property bool isDisabled: layoutIndex > 3 + + StateLayer { + id: layer + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + implicitHeight: parent.height - 4 + + radius: Appearance.rounding.full + enabled: !isDisabled + + function onClicked(): void { + if (!isDisabled) + kb.switchTo(layoutIndex); + } + } + + StyledText { + id: rowText + anchors.verticalCenter: layer.verticalCenter + anchors.left: layer.left + anchors.right: layer.right + anchors.leftMargin: Appearance.padding.small + anchors.rightMargin: Appearance.padding.small + text: label + elide: Text.ElideRight + opacity: isDisabled ? 0.4 : 1.0 + } + + ToolTip.visible: isDisabled && layer.containsMouse + ToolTip.text: "XKB limitation: maximum 4 layouts allowed" + } + } + + Rectangle { + visible: kb.activeLabel.length > 0 + Layout.fillWidth: true + Layout.rightMargin: Appearance.padding.small + Layout.topMargin: Appearance.spacing.small + + height: 1 + color: Colours.palette.m3onSurfaceVariant + opacity: 0.35 + } + + RowLayout { + id: activeRow + + visible: kb.activeLabel.length > 0 + Layout.fillWidth: true + Layout.rightMargin: Appearance.padding.small + Layout.topMargin: Appearance.spacing.small + spacing: Appearance.spacing.small + + opacity: 1 + scale: 1 + + MaterialIcon { + text: "keyboard" + color: Colours.palette.m3primary + } + + StyledText { + Layout.fillWidth: true + text: kb.activeLabel + elide: Text.ElideRight + font.weight: 500 + color: Colours.palette.m3primary + } + + Connections { + target: kb + function onActiveLabelChanged() { + if (!activeRow.visible) + return; + popIn.restart(); + } + } + + SequentialAnimation { + id: popIn + running: false + + ParallelAnimation { + NumberAnimation { target: activeRow; property: "opacity"; to: 0.0; duration: 70 } + NumberAnimation { target: activeRow; property: "scale"; to: 0.92; duration: 70 } + } + + ParallelAnimation { + NumberAnimation { target: activeRow; property: "opacity"; to: 1.0; duration: 160; easing.type: Easing.OutCubic } + NumberAnimation { target: activeRow; property: "scale"; to: 1.0; duration: 220; easing.type: Easing.OutBack } + } + } + } +} diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml new file mode 100644 index 0000000..41e45b3 --- /dev/null +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -0,0 +1,200 @@ +pragma ComponentBehavior: Bound + +import QtQuick + +import Quickshell +import Quickshell.Io + +import qs.config +import Caelestia + +Item { + id: model + visible: false + + ListModel { id: _visibleModel } + property alias visibleModel: _visibleModel + + property string activeLabel: "" + property int activeIndex: -1 + + function start() { + _xkbXmlBase.running = true; + _getKbLayoutOpt.running = true; + } + + function refresh() { + _notifiedLimit = false; + _getKbLayoutOpt.running = true; + } + + function switchTo(idx) { + _switchProc.command = ["hyprctl", "switchxkblayout", "all", String(idx)]; + _switchProc.running = true; + } + + ListModel { id: _layoutsModel } + + property var _xkbMap: ({}) + property bool _notifiedLimit: false + + Process { + id: _xkbXmlBase + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] + stdout: StdioCollector { onStreamFinished: _buildXmlMap(text) } + onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0) _xkbXmlEvdev.running = true + } + + Process { + id: _xkbXmlEvdev + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] + stdout: StdioCollector { onStreamFinished: _buildXmlMap(text) } + } + + function _buildXmlMap(xml) { + const map = {}; + + const re = /<name>\s*([^<]+?)\s*<\/name>[\s\S]*?<description>\s*([^<]+?)\s*<\/description>/g; + + let m; + while ((m = re.exec(xml)) !== null) { + const code = (m[1] || "").trim(); + const desc = (m[2] || "").trim(); + if (!code || !desc) continue; + map[code] = _short(desc); + } + + if (Object.keys(map).length === 0) + return; + + _xkbMap = map; + + if (_layoutsModel.count > 0) { + const tmp = []; + for (let i = 0; i < _layoutsModel.count; i++) { + const it = _layoutsModel.get(i); + tmp.push({ layoutIndex: it.layoutIndex, token: it.token, label: _pretty(it.token) }); + } + _layoutsModel.clear(); + tmp.forEach(t => _layoutsModel.append(t)); + _fetchActiveLayouts.running = true; + } + } + + function _short(desc) { + const m = desc.match(/^(.*)\((.*)\)$/); + if (!m) return desc; + const lang = m[1].trim(); + const region = m[2].trim(); + const code = (region.split(/[,\s-]/)[0] || region).slice(0, 2).toUpperCase(); + return `${lang} (${code})`; + } + + Process { + id: _getKbLayoutOpt + command: ["hyprctl", "-j", "getoption", "input:kb_layout"] + stdout: StdioCollector { + onStreamFinished: { + try { + const j = JSON.parse(text); + const raw = (j?.str || j?.value || "").toString().trim(); + if (raw.length) { + _setLayouts(raw); + _fetchActiveLayouts.running = true; + return; + } + } catch (e) {} + _fetchLayoutsFromDevices.running = true; + } + } + } + + Process { + id: _fetchLayoutsFromDevices + command: ["hyprctl", "-j", "devices"] + stdout: StdioCollector { + onStreamFinished: { + try { + const dev = JSON.parse(text); + const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; + const raw = (kb?.layout || "").trim(); + if (raw.length) _setLayouts(raw); + } catch (e) {} + _fetchActiveLayouts.running = true; + } + } + } + + Process { + id: _fetchActiveLayouts + command: ["hyprctl", "-j", "devices"] + stdout: StdioCollector { + onStreamFinished: { + try { + const dev = JSON.parse(text); + const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; + const idx = kb?.active_layout_index ?? -1; + + activeIndex = idx >= 0 ? idx : -1; + activeLabel = + (idx >= 0 && idx < _layoutsModel.count) + ? _layoutsModel.get(idx).label + : ""; + } catch (e) { + activeIndex = -1; + activeLabel = ""; + } + + _rebuildVisible(); + } + } + } + + Process { + id: _switchProc + onRunningChanged: if (!running) _fetchActiveLayouts.running = true + } + + function _setLayouts(raw) { + const parts = raw.split(",").map(s => s.trim()).filter(Boolean); + _layoutsModel.clear(); + + const seen = new Set(); + let idx = 0; + + for (const p of parts) { + if (seen.has(p)) continue; + seen.add(p); + _layoutsModel.append({ layoutIndex: idx, token: p, label: _pretty(p) }); + idx++; + } + } + + function _rebuildVisible() { + _visibleModel.clear(); + + let arr = []; + for (let i = 0; i < _layoutsModel.count; i++) + arr.push(_layoutsModel.get(i)); + + arr = arr.filter(i => i.layoutIndex !== activeIndex); + arr.forEach(i => _visibleModel.append(i)); + + if (!Config.utilities.toasts.kbLimit) + return; + + if (_layoutsModel.count > 4) { + Toaster.toast( + qsTr("Keyboard layout limit"), + qsTr("XKB supports only 4 layouts at a time"), + "warning" + ); + } + } + + function _pretty(token) { + const code = token.replace(/\(.*\)$/, "").trim(); + if (_xkbMap[code]) return code.toUpperCase() + " - " + _xkbMap[code]; + return code.toUpperCase() + " - " + code; + } +} |