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>
This commit is contained in:
parent
146aeb7e74
commit
60a567bbba
7 changed files with 367 additions and 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
|
||||
|
|
|
|||
|
|
@ -111,5 +111,6 @@ JsonObject {
|
|||
property int trayMenuWidth: 300
|
||||
property int batteryWidth: 250
|
||||
property int networkWidth: 320
|
||||
property int kbLayoutWidth: 320
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import Quickshell
|
|||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
|
||||
import "./kblayout"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
|
|
@ -114,8 +116,11 @@ Item {
|
|||
|
||||
Popout {
|
||||
name: "kblayout"
|
||||
sourceComponent: KbLayout {}
|
||||
sourceComponent: KbLayout {
|
||||
wrapper: root.wrapper
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Popout {
|
||||
name: "lockstatus"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
158
modules/bar/popouts/kblayout/KbLayout.qml
Normal file
158
modules/bar/popouts/kblayout/KbLayout.qml
Normal file
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
200
modules/bar/popouts/kblayout/KbLayoutModel.qml
Normal file
200
modules/bar/popouts/kblayout/KbLayoutModel.qml
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue