456 lines
16 KiB
QML
456 lines
16 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell.Io
|
|
import qs.Commons
|
|
import qs.Widgets
|
|
|
|
ColumnLayout {
|
|
id: root
|
|
spacing: Style.marginL
|
|
|
|
property var pluginApi: null
|
|
|
|
// Requirement check states
|
|
property bool evtestInstalled: false
|
|
property bool inInputGroup: false
|
|
property string currentUser: ""
|
|
|
|
// Editable settings properties
|
|
property string editCatColor: {
|
|
let saved = pluginApi?.pluginSettings?.catColor
|
|
if (saved && saved.length > 0) return saved
|
|
return pluginApi?.manifest?.metadata?.defaultSettings?.catColor ?? "none"
|
|
}
|
|
|
|
property real editCatSize: {
|
|
let saved = pluginApi?.pluginSettings?.catSize
|
|
if (saved !== undefined && saved !== null) return saved
|
|
return pluginApi?.manifest?.metadata?.defaultSettings?.catSize ?? 1.0
|
|
}
|
|
|
|
property real editCatOffsetY: {
|
|
let saved = pluginApi?.pluginSettings?.catOffsetY
|
|
if (saved !== undefined && saved !== null) return saved
|
|
return pluginApi?.manifest?.metadata?.defaultSettings?.catOffsetY ?? 0.11
|
|
}
|
|
|
|
property var editInputDevices: {
|
|
let saved = pluginApi?.pluginSettings?.inputDevices
|
|
if (saved && saved.length > 0) return saved
|
|
let legacy = pluginApi?.pluginSettings?.inputDevice
|
|
?? pluginApi?.manifest?.metadata?.defaultSettings?.inputDevice
|
|
return legacy ? [legacy] : []
|
|
}
|
|
|
|
property bool editRaveMode: {
|
|
let saved = pluginApi?.pluginSettings?.raveMode
|
|
if (saved !== undefined && saved !== null) return saved
|
|
return pluginApi?.manifest?.metadata?.defaultSettings?.raveMode ?? false
|
|
}
|
|
|
|
property bool editTappyMode: {
|
|
let saved = pluginApi?.pluginSettings?.tappyMode
|
|
if (saved !== undefined && saved !== null) return saved
|
|
return pluginApi?.manifest?.metadata?.defaultSettings?.tappyMode ?? false
|
|
}
|
|
|
|
property bool editUseMprisFilter: {
|
|
let saved = pluginApi?.pluginSettings?.useMprisFilter
|
|
if (saved !== undefined && saved !== null) return saved
|
|
return pluginApi?.manifest?.metadata?.defaultSettings?.useMprisFilter ?? false
|
|
}
|
|
|
|
// Status colors (with fallback for theme compatibility)
|
|
readonly property color statusSuccessColor: Color.mPrimary
|
|
readonly property color statusErrorColor: Color.mError ?? "#c00202"
|
|
|
|
property var inputDevices: []
|
|
|
|
function isSelected(key) {
|
|
return root.editInputDevices.indexOf(key) >= 0
|
|
}
|
|
|
|
function toggleDevice(key) {
|
|
let list = root.editInputDevices.slice()
|
|
let idx = list.indexOf(key)
|
|
if (idx >= 0)
|
|
list.splice(idx, 1)
|
|
else
|
|
list.push(key)
|
|
root.editInputDevices = list
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
evtestCheck.running = true
|
|
userCheck.running = true
|
|
byIdListProcess.running = true
|
|
}
|
|
|
|
Process {
|
|
id: evtestCheck
|
|
command: ["which", "evtest"]
|
|
stdout: StdioCollector {}
|
|
stderr: StdioCollector {}
|
|
onExited: function(exitCode, exitStatus) {
|
|
root.evtestInstalled = (exitCode == 0)
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: userCheck
|
|
command: ["id", "-un"]
|
|
stdout: SplitParser {
|
|
onRead: data => {
|
|
root.currentUser = data.trim()
|
|
}
|
|
}
|
|
stderr: StdioCollector {}
|
|
onExited: function(exitCode, exitStatus) {
|
|
if (exitCode == 0 && root.currentUser.length > 0)
|
|
groupCheck.running = true
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: groupCheck
|
|
command: ["sh", "-c", "id -nG '" + root.currentUser + "' | tr ' ' '\\n' | grep -qx input"]
|
|
stdout: StdioCollector {}
|
|
stderr: StdioCollector {}
|
|
onExited: function(exitCode, exitStatus) {
|
|
root.inInputGroup = (exitCode == 0)
|
|
}
|
|
}
|
|
|
|
// Try by-id first
|
|
Process {
|
|
id: byIdListProcess
|
|
command: ["sh", "-c", "[ -d /dev/input/by-id ] && for f in /dev/input/by-id/*-event-*; do [ -e \"$f\" ] && echo \"$(basename \"$f\")|$(readlink -f \"$f\")\"; done || true"]
|
|
|
|
stdout: SplitParser {
|
|
onRead: data => {
|
|
const line = data.trim()
|
|
if (line.length === 0) return
|
|
const parts = line.split("|")
|
|
if (parts.length !== 2) return
|
|
const name = parts[0]
|
|
const resolved = parts[1]
|
|
if (!resolved.startsWith("/dev/input/event")) return
|
|
|
|
const eventNum = resolved.replace(/.*\//, "")
|
|
let friendly = name
|
|
.replace(/^usb-/, "")
|
|
.replace(/-event-\w+$/, "")
|
|
.replace(/-if\d+$/, "")
|
|
.replace(/_/g, " ")
|
|
|
|
root.inputDevices = root.inputDevices.concat([{
|
|
key: resolved,
|
|
name: friendly,
|
|
eventDev: eventNum
|
|
}])
|
|
}
|
|
}
|
|
|
|
stderr: StdioCollector {}
|
|
|
|
onExited: function(exitCode, exitStatus) {
|
|
// Always try to get names from sysfs
|
|
sysfsListProcess.running = true
|
|
}
|
|
}
|
|
|
|
// Get device names from sysfs
|
|
Process {
|
|
id: sysfsListProcess
|
|
command: ["sh", "-c", "for f in /dev/input/event*; do [ -c \"$f\" ] && echo \"$f|$(cat /sys/class/input/$(basename $f)/device/name 2>/dev/null || basename $f)\"; done"]
|
|
running: false
|
|
|
|
stdout: SplitParser {
|
|
onRead: data => {
|
|
const line = data.trim()
|
|
if (line.length === 0) return
|
|
const parts = line.split("|")
|
|
if (parts.length !== 2) return
|
|
const device = parts[0]
|
|
const name = parts[1]
|
|
const eventNum = device.replace(/.*\//, "")
|
|
|
|
// Filter out non-keyboardy devices
|
|
const nameLower = name.toLowerCase()
|
|
const excludePatterns = [
|
|
/power button/i,
|
|
/sleep button/i,
|
|
/lid switch/i,
|
|
/video bus/i,
|
|
/audio/i,
|
|
/hdmi/i,
|
|
/speaker/i,
|
|
/headphone/i,
|
|
/mic\b/i
|
|
]
|
|
|
|
const shouldExclude = excludePatterns.some(pattern => pattern.test(name))
|
|
if (shouldExclude) return
|
|
|
|
// Check if we already have this device from by-id
|
|
const exists = root.inputDevices.some(d => d.key === device)
|
|
if (!exists) {
|
|
root.inputDevices = root.inputDevices.concat([{
|
|
key: device,
|
|
name: name,
|
|
eventDev: eventNum
|
|
}])
|
|
}
|
|
}
|
|
}
|
|
|
|
stderr: StdioCollector {}
|
|
}
|
|
|
|
// Requirements Section
|
|
Text {
|
|
text: pluginApi?.tr("settings.requirements")
|
|
color: Color.mOnSurface
|
|
font.pointSize: Style.fontSizeM
|
|
font.weight: Font.DemiBold
|
|
}
|
|
|
|
NBox {
|
|
Layout.fillWidth: true
|
|
implicitHeight: reqContent.implicitHeight + Style.marginM * 2
|
|
|
|
ColumnLayout {
|
|
id: reqContent
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: Style.marginM
|
|
spacing: Style.marginS
|
|
|
|
RowLayout {
|
|
spacing: Style.marginS
|
|
NIcon {
|
|
icon: root.evtestInstalled ? "circle-check-filled" : "circle-x-filled"
|
|
color: root.evtestInstalled ? root.statusSuccessColor : root.statusErrorColor
|
|
pointSize: Style.fontSizeM
|
|
}
|
|
Text {
|
|
text: root.evtestInstalled
|
|
? pluginApi?.tr("settings.evtest-installed")
|
|
: pluginApi?.tr("settings.evtest-not-installed")
|
|
color: root.evtestInstalled ? root.statusSuccessColor : root.statusErrorColor
|
|
font.pointSize: Style.fontSizeM
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: Style.marginS
|
|
NIcon {
|
|
icon: root.inInputGroup ? "circle-check-filled" : "circle-x-filled"
|
|
color: root.inInputGroup ? root.statusSuccessColor : root.statusErrorColor
|
|
pointSize: Style.fontSizeM
|
|
}
|
|
Text {
|
|
text: root.inInputGroup
|
|
? pluginApi?.tr("settings.in-input-group")
|
|
: pluginApi?.tr("settings.not-in-input-group")
|
|
color: root.inInputGroup ? root.statusSuccessColor : root.statusErrorColor
|
|
font.pointSize: Style.fontSizeM
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NDivider {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
// Widget Color
|
|
NColorChoice {
|
|
label: pluginApi?.tr("settings.colours")
|
|
currentKey: root.editCatColor
|
|
onSelected: key => { root.editCatColor = key; }
|
|
}
|
|
|
|
// Cat Size Section
|
|
NValueSlider {
|
|
Layout.fillWidth: true
|
|
label: pluginApi?.tr("settings.cat-size")
|
|
value: root.editCatSize
|
|
from: 0.5
|
|
to: 1.5
|
|
stepSize: 0.01
|
|
defaultValue: 1.0
|
|
showReset: true
|
|
text: Math.round(root.editCatSize * 100) + "%"
|
|
onMoved: value => root.editCatSize = value
|
|
}
|
|
|
|
// Vertical Position Section
|
|
NValueSlider {
|
|
Layout.fillWidth: true
|
|
label: pluginApi?.tr("settings.vertical-position")
|
|
value: root.editCatOffsetY
|
|
from: -0.39
|
|
to: 0.61
|
|
stepSize: 0.01
|
|
defaultValue: 0.11
|
|
showReset: true
|
|
text: { let v = Math.round(-(root.editCatOffsetY - 0.11) * 100) / 100; return (v > 0 ? "+" : "") + v.toFixed(2) }
|
|
onMoved: value => root.editCatOffsetY = value
|
|
}
|
|
|
|
// Rave Mode
|
|
NToggle {
|
|
label: pluginApi?.tr("settings.rave-mode")
|
|
description: pluginApi?.tr("settings.rave-mode-desc")
|
|
checked: root.editRaveMode
|
|
onToggled: checked => root.editRaveMode = checked
|
|
defaultValue: pluginApi?.manifest?.metadata?.defaultSettings?.raveMode ?? false
|
|
}
|
|
|
|
// Tappy Mode
|
|
NToggle {
|
|
label: pluginApi?.tr("settings.tappy-mode")
|
|
description: pluginApi?.tr("settings.tappy-mode-desc")
|
|
checked: root.editTappyMode
|
|
onToggled: checked => root.editTappyMode = checked
|
|
defaultValue: pluginApi?.manifest?.metadata?.defaultSettings?.tappyMode ?? false
|
|
}
|
|
|
|
// MPRIS Filtering
|
|
NToggle {
|
|
label: pluginApi?.tr("settings.mpris-filter")
|
|
description: pluginApi?.tr("settings.mpris-filter-desc")
|
|
checked: root.editUseMprisFilter
|
|
onToggled: checked => root.editUseMprisFilter = checked
|
|
defaultValue: pluginApi?.manifest?.metadata?.defaultSettings?.useMprisFilter ?? false
|
|
}
|
|
|
|
NDivider {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
// Input Devices Section
|
|
Text {
|
|
text: pluginApi?.tr("settings.input-devices") || "Input Devices"
|
|
color: Color.mOnSurface
|
|
font.pointSize: Style.fontSizeM
|
|
font.weight: Font.DemiBold
|
|
}
|
|
|
|
NBox {
|
|
Layout.fillWidth: true
|
|
implicitHeight: devContent.implicitHeight + Style.marginM * 2
|
|
|
|
ColumnLayout {
|
|
id: devContent
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: Style.marginM
|
|
spacing: Style.marginS
|
|
|
|
Repeater {
|
|
model: root.inputDevices
|
|
|
|
Rectangle {
|
|
required property var modelData
|
|
|
|
property bool isChecked: root.isSelected(modelData.key)
|
|
property bool isHovered: mouseArea.containsMouse
|
|
|
|
Layout.fillWidth: true
|
|
implicitHeight: rowContent.implicitHeight + Style.marginS * 2
|
|
radius: Style.iRadiusXS
|
|
color: isHovered ? Color.mSurfaceVariant : "transparent"
|
|
|
|
Behavior on color {
|
|
ColorAnimation { duration: Style.animationFast }
|
|
}
|
|
|
|
MouseArea {
|
|
id: mouseArea
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
hoverEnabled: true
|
|
onClicked: root.toggleDevice(modelData.key)
|
|
}
|
|
|
|
RowLayout {
|
|
id: rowContent
|
|
anchors.fill: parent
|
|
anchors.leftMargin: Style.marginS
|
|
anchors.rightMargin: Style.marginS
|
|
spacing: Style.marginM
|
|
|
|
Rectangle {
|
|
id: checkBox
|
|
implicitWidth: Math.round(Style.baseWidgetSize * 0.7)
|
|
implicitHeight: Math.round(Style.baseWidgetSize * 0.7)
|
|
radius: Style.iRadiusXS
|
|
color: parent.parent.isChecked ? Color.mPrimary : Color.mSurface
|
|
border.color: parent.parent.isHovered ? Color.mPrimary : Color.mOutline
|
|
border.width: Style.borderS
|
|
|
|
Behavior on color {
|
|
ColorAnimation { duration: Style.animationFast }
|
|
}
|
|
Behavior on border.color {
|
|
ColorAnimation { duration: Style.animationFast }
|
|
}
|
|
|
|
NIcon {
|
|
visible: parent.parent.parent.isChecked
|
|
anchors.centerIn: parent
|
|
anchors.horizontalCenterOffset: -1
|
|
icon: "check"
|
|
color: Color.mOnPrimary
|
|
pointSize: Math.max(Style.fontSizeXS, checkBox.implicitWidth * 0.5)
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 2
|
|
|
|
Text {
|
|
text: parent.parent.parent.modelData.name
|
|
color: Color.mOnSurface
|
|
font.pointSize: Style.fontSizeM
|
|
elide: Text.ElideRight
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Text {
|
|
text: parent.parent.parent.modelData.eventDev
|
|
color: Color.mOnSurfaceVariant
|
|
font.pointSize: Style.fontSizeS
|
|
visible: text !== ""
|
|
elide: Text.ElideRight
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function saveSettings() {
|
|
if (!pluginApi) {
|
|
Logger.e("Slow Bongo", "Cannot save settings: pluginApi is null")
|
|
return
|
|
}
|
|
pluginApi.pluginSettings.inputDevices = root.editInputDevices
|
|
pluginApi.pluginSettings.catColor = root.editCatColor
|
|
pluginApi.pluginSettings.catSize = root.editCatSize
|
|
pluginApi.pluginSettings.catOffsetY = root.editCatOffsetY
|
|
pluginApi.pluginSettings.raveMode = root.editRaveMode
|
|
pluginApi.pluginSettings.tappyMode = root.editTappyMode
|
|
pluginApi.pluginSettings.useMprisFilter = root.editUseMprisFilter
|
|
pluginApi.saveSettings()
|
|
Logger.i("Slow Bongo", "Settings saved successfully")
|
|
}
|
|
}
|