mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
feat(settings): add Mouse & Touchpad settings tab for Niri (#2905)
* feat(settings): add Mouse & Trackpad settings tab * style(settings): clarify touchpad natural scroll description * chore(settings): update settings search index * refactor(settings): unify Mouse & Trackpad terminology to Mouse & Touchpad * feat(setup): deploy default dms/input.kdl on setup * style(niri): restore include section comment in base config template * refactor(niri): remove redundant input block from base config template * refactor(niri): restore keyboard and non-mouse input config to base template * feat(settings): add touchpad disable-on-external-mouse toggle * feat(settings): add left-handed, scroll-factor and dwt options * feat(settings): add middle-emulation, scroll-method, tap-and-drag and drag-lock options * refactor(settings): re-order mouse and touchpad UI options logically
This commit is contained in:
@@ -40,6 +40,7 @@ var resolveIncludeCmd = &cobra.Command{
|
|||||||
"layout.kdl",
|
"layout.kdl",
|
||||||
"outputs.kdl",
|
"outputs.kdl",
|
||||||
"binds.kdl",
|
"binds.kdl",
|
||||||
|
"input.kdl",
|
||||||
"cursor.conf",
|
"cursor.conf",
|
||||||
"layout.conf",
|
"layout.conf",
|
||||||
"outputs.conf",
|
"outputs.conf",
|
||||||
|
|||||||
@@ -143,6 +143,10 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
|
|||||||
niriFile: "alttab.kdl",
|
niriFile: "alttab.kdl",
|
||||||
niriContent: func(_ string) string { return config.NiriAlttabConfig },
|
niriContent: func(_ string) string { return config.NiriAlttabConfig },
|
||||||
},
|
},
|
||||||
|
"input": {
|
||||||
|
niriFile: "input.kdl",
|
||||||
|
niriContent: func(_ string) string { return config.NiriInputConfig },
|
||||||
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
niriFile: "outputs.kdl",
|
niriFile: "outputs.kdl",
|
||||||
hyprFile: "outputs.lua",
|
hyprFile: "outputs.lua",
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
{"layout.kdl", NiriLayoutConfig},
|
{"layout.kdl", NiriLayoutConfig},
|
||||||
{"alttab.kdl", NiriAlttabConfig},
|
{"alttab.kdl", NiriAlttabConfig},
|
||||||
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
|
{"input.kdl", NiriInputConfig},
|
||||||
{"outputs.kdl", ""},
|
{"outputs.kdl", ""},
|
||||||
{"cursor.kdl", ""},
|
{"cursor.kdl", ""},
|
||||||
{"windowrules.kdl", ""},
|
{"windowrules.kdl", ""},
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// ! DO NOT EDIT !
|
||||||
|
// ! AUTO-GENERATED BY DMS !
|
||||||
|
// ! CHANGES WILL BE OVERWRITTEN !
|
||||||
|
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||||
|
|
||||||
|
input {
|
||||||
|
mouse {
|
||||||
|
accel-speed 0.0
|
||||||
|
}
|
||||||
|
touchpad {
|
||||||
|
tap
|
||||||
|
accel-speed 0.0
|
||||||
|
natural-scroll
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,31 +34,6 @@ input {
|
|||||||
numlock
|
numlock
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next sections include libinput settings.
|
|
||||||
// Omitting settings disables them, or leaves them at their default values.
|
|
||||||
// All commented-out settings here are examples, not defaults.
|
|
||||||
touchpad {
|
|
||||||
// off
|
|
||||||
tap
|
|
||||||
// dwt
|
|
||||||
// dwtp
|
|
||||||
// drag false
|
|
||||||
// drag-lock
|
|
||||||
natural-scroll
|
|
||||||
// accel-speed 0.2
|
|
||||||
// accel-profile "flat"
|
|
||||||
// scroll-method "two-finger"
|
|
||||||
// disabled-on-external-mouse
|
|
||||||
}
|
|
||||||
|
|
||||||
mouse {
|
|
||||||
// off
|
|
||||||
// natural-scroll
|
|
||||||
// accel-speed 0.2
|
|
||||||
// accel-profile "flat"
|
|
||||||
// scroll-method "no-scroll"
|
|
||||||
}
|
|
||||||
|
|
||||||
trackpoint {
|
trackpoint {
|
||||||
// off
|
// off
|
||||||
// natural-scroll
|
// natural-scroll
|
||||||
@@ -77,6 +52,7 @@ input {
|
|||||||
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
||||||
// focus-follows-mouse max-scroll-amount="0%"
|
// focus-follows-mouse max-scroll-amount="0%"
|
||||||
}
|
}
|
||||||
|
|
||||||
// You can configure outputs by their name, which you can find
|
// You can configure outputs by their name, which you can find
|
||||||
// by running `niri msg outputs` while inside a niri instance.
|
// by running `niri msg outputs` while inside a niri instance.
|
||||||
// The built-in laptop monitor is usually called "eDP-1".
|
// The built-in laptop monitor is usually called "eDP-1".
|
||||||
@@ -271,3 +247,4 @@ include optional=true "dms/alttab.kdl"
|
|||||||
include optional=true "dms/binds.kdl"
|
include optional=true "dms/binds.kdl"
|
||||||
include optional=true "dms/outputs.kdl"
|
include optional=true "dms/outputs.kdl"
|
||||||
include optional=true "dms/cursor.kdl"
|
include optional=true "dms/cursor.kdl"
|
||||||
|
include optional=true "dms/input.kdl"
|
||||||
|
|||||||
@@ -16,3 +16,6 @@ var NiriAlttabConfig string
|
|||||||
|
|
||||||
//go:embed embedded/niri-binds.kdl
|
//go:embed embedded/niri-binds.kdl
|
||||||
var NiriBindsConfig string
|
var NiriBindsConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/niri-input.kdl
|
||||||
|
var NiriInputConfig string
|
||||||
|
|||||||
@@ -184,6 +184,24 @@ Singleton {
|
|||||||
property int mangoLayoutRadiusOverride: -1
|
property int mangoLayoutRadiusOverride: -1
|
||||||
property int mangoLayoutBorderSize: -1
|
property int mangoLayoutBorderSize: -1
|
||||||
property bool mangoTrackpadNaturalScrolling: true
|
property bool mangoTrackpadNaturalScrolling: true
|
||||||
|
property string mouseAccelProfile: "default"
|
||||||
|
property real mouseAccelSpeed: 0.0
|
||||||
|
property bool mouseLeftHanded: false
|
||||||
|
property bool mouseMiddleEmulation: false
|
||||||
|
property bool mouseNaturalScroll: false
|
||||||
|
property real mouseScrollFactor: 1.0
|
||||||
|
property string mouseScrollMethod: "default"
|
||||||
|
property string touchpadAccelProfile: "default"
|
||||||
|
property real touchpadAccelSpeed: 0.0
|
||||||
|
property bool touchpadDisableOnExternalMouse: false
|
||||||
|
property bool touchpadDisableWhileTyping: true
|
||||||
|
property bool touchpadDragLock: false
|
||||||
|
property bool touchpadMiddleEmulation: false
|
||||||
|
property bool touchpadNaturalScroll: true
|
||||||
|
property real touchpadScrollFactor: 1.0
|
||||||
|
property string touchpadScrollMethod: "default"
|
||||||
|
property bool touchpadTapAndDrag: true
|
||||||
|
property bool touchpadTapToClick: true
|
||||||
|
|
||||||
property int firstDayOfWeek: -1
|
property int firstDayOfWeek: -1
|
||||||
property bool showWeekNumber: false
|
property bool showWeekNumber: false
|
||||||
@@ -1479,6 +1497,13 @@ Singleton {
|
|||||||
MangoService.generateLayoutConfig();
|
MangoService.generateLayoutConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateCompositorInput() {
|
||||||
|
if (typeof CompositorService === "undefined")
|
||||||
|
return;
|
||||||
|
if (CompositorService.isNiri && typeof NiriService !== "undefined")
|
||||||
|
NiriService.generateNiriInputConfig();
|
||||||
|
}
|
||||||
|
|
||||||
function updateFrameCompositorLayout() {
|
function updateFrameCompositorLayout() {
|
||||||
// Generate before begin() so compositor readiness is already pending at transitionRequested
|
// Generate before begin() so compositor readiness is already pending at transitionRequested
|
||||||
if (typeof CompositorService !== "undefined") {
|
if (typeof CompositorService !== "undefined") {
|
||||||
@@ -1707,6 +1732,7 @@ Singleton {
|
|||||||
"applyStoredTheme": applyStoredTheme,
|
"applyStoredTheme": applyStoredTheme,
|
||||||
"regenSystemThemes": regenSystemThemes,
|
"regenSystemThemes": regenSystemThemes,
|
||||||
"updateCompositorLayout": updateCompositorLayout,
|
"updateCompositorLayout": updateCompositorLayout,
|
||||||
|
"updateCompositorInput": updateCompositorInput,
|
||||||
"applyStoredIconTheme": applyStoredIconTheme,
|
"applyStoredIconTheme": applyStoredIconTheme,
|
||||||
"updateBarConfigs": updateBarConfigs,
|
"updateBarConfigs": updateBarConfigs,
|
||||||
"updateCompositorCursor": updateCompositorCursor,
|
"updateCompositorCursor": updateCompositorCursor,
|
||||||
|
|||||||
@@ -38,6 +38,24 @@ var SPEC = {
|
|||||||
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
mangoTrackpadNaturalScrolling: { def: true, onChange: "updateCompositorCursor" },
|
mangoTrackpadNaturalScrolling: { def: true, onChange: "updateCompositorCursor" },
|
||||||
|
mouseAccelProfile: { def: "default", onChange: "updateCompositorInput" },
|
||||||
|
mouseAccelSpeed: { def: 0.0, onChange: "updateCompositorInput" },
|
||||||
|
mouseLeftHanded: { def: false, onChange: "updateCompositorInput" },
|
||||||
|
mouseMiddleEmulation: { def: false, onChange: "updateCompositorInput" },
|
||||||
|
mouseNaturalScroll: { def: false, onChange: "updateCompositorInput" },
|
||||||
|
mouseScrollFactor: { def: 1.0, onChange: "updateCompositorInput" },
|
||||||
|
mouseScrollMethod: { def: "default", onChange: "updateCompositorInput" },
|
||||||
|
touchpadAccelProfile: { def: "default", onChange: "updateCompositorInput" },
|
||||||
|
touchpadAccelSpeed: { def: 0.0, onChange: "updateCompositorInput" },
|
||||||
|
touchpadDisableOnExternalMouse: { def: false, onChange: "updateCompositorInput" },
|
||||||
|
touchpadDisableWhileTyping: { def: true, onChange: "updateCompositorInput" },
|
||||||
|
touchpadDragLock: { def: false, onChange: "updateCompositorInput" },
|
||||||
|
touchpadMiddleEmulation: { def: false, onChange: "updateCompositorInput" },
|
||||||
|
touchpadNaturalScroll: { def: true, onChange: "updateCompositorInput" },
|
||||||
|
touchpadScrollFactor: { def: 1.0, onChange: "updateCompositorInput" },
|
||||||
|
touchpadScrollMethod: { def: "default", onChange: "updateCompositorInput" },
|
||||||
|
touchpadTapAndDrag: { def: true, onChange: "updateCompositorInput" },
|
||||||
|
touchpadTapToClick: { def: true, onChange: "updateCompositorInput" },
|
||||||
|
|
||||||
firstDayOfWeek: { def: -1 },
|
firstDayOfWeek: { def: -1 },
|
||||||
showWeekNumber: { def: false },
|
showWeekNumber: { def: false },
|
||||||
|
|||||||
@@ -718,5 +718,20 @@ FocusScope {
|
|||||||
Qt.callLater(() => item.forceActiveFocus());
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: mouseTouchpadLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 44
|
||||||
|
visible: active
|
||||||
|
focus: active
|
||||||
|
|
||||||
|
sourceComponent: MouseTouchpadTab {}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active && item)
|
||||||
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,6 +319,13 @@ Rectangle {
|
|||||||
"icon": "headphones",
|
"icon": "headphones",
|
||||||
"tabIndex": 29
|
"tabIndex": 29
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "mouse_touchpad",
|
||||||
|
"text": I18n.tr("Mouse & Touchpad"),
|
||||||
|
"icon": "mouse",
|
||||||
|
"tabIndex": 44,
|
||||||
|
"niriOnly": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "locale",
|
"id": "locale",
|
||||||
"text": I18n.tr("Locale"),
|
"text": I18n.tr("Locale"),
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
import QtCore
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.Settings.Widgets
|
||||||
|
import "../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
LayoutMirroring.enabled: I18n.isRtl
|
||||||
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
|
property var inputIncludeStatus: ({
|
||||||
|
"exists": false,
|
||||||
|
"included": false,
|
||||||
|
"configFormat": "",
|
||||||
|
"readOnly": false
|
||||||
|
})
|
||||||
|
property bool checkingInclude: false
|
||||||
|
property bool fixingInclude: false
|
||||||
|
|
||||||
|
function getInputConfigPaths() {
|
||||||
|
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
|
||||||
|
if (CompositorService.compositor !== "niri")
|
||||||
|
return null;
|
||||||
|
return {
|
||||||
|
"configFile": configDir + "/niri/config.kdl",
|
||||||
|
"layoutFile": configDir + "/niri/dms/input.kdl",
|
||||||
|
"grepPattern": 'include.*"dms/input.kdl"',
|
||||||
|
"includeLine": 'include "dms/input.kdl"'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkInputIncludeStatus() {
|
||||||
|
if (CompositorService.compositor !== "niri") {
|
||||||
|
inputIncludeStatus = {
|
||||||
|
"exists": false,
|
||||||
|
"included": false,
|
||||||
|
"configFormat": "",
|
||||||
|
"readOnly": false
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkingInclude = true;
|
||||||
|
Proc.runCommand("check-input-include", [Proc.dmsBin, "config", "resolve-include", "niri", "input.kdl"], (output, exitCode) => {
|
||||||
|
checkingInclude = false;
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
inputIncludeStatus = {
|
||||||
|
"exists": false,
|
||||||
|
"included": false,
|
||||||
|
"configFormat": "",
|
||||||
|
"readOnly": false
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
inputIncludeStatus = JSON.parse(output.trim());
|
||||||
|
} catch (e) {
|
||||||
|
inputIncludeStatus = {
|
||||||
|
"exists": false,
|
||||||
|
"included": false,
|
||||||
|
"configFormat": "",
|
||||||
|
"readOnly": false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixInputInclude() {
|
||||||
|
const paths = getInputConfigPaths();
|
||||||
|
if (!paths)
|
||||||
|
return;
|
||||||
|
|
||||||
|
fixingInclude = true;
|
||||||
|
const unixTime = Math.floor(Date.now() / 1000);
|
||||||
|
const backupFile = paths.configFile + ".backup" + unixTime;
|
||||||
|
const script = ConfigIncludeResolve.buildRepairScript({
|
||||||
|
configFile: paths.configFile,
|
||||||
|
backupFile: backupFile,
|
||||||
|
fragmentFile: paths.layoutFile,
|
||||||
|
grepPattern: paths.grepPattern,
|
||||||
|
includeLine: paths.includeLine
|
||||||
|
});
|
||||||
|
Proc.runCommand("fix-input-include", ["sh", "-c", script], (output, exitCode) => {
|
||||||
|
fixingInclude = false;
|
||||||
|
if (exitCode !== 0)
|
||||||
|
return;
|
||||||
|
checkInputIncludeStatus();
|
||||||
|
SettingsData.updateCompositorInput();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (CompositorService.isNiri) {
|
||||||
|
checkInputIncludeStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
contentHeight: settingsColumn.height + Theme.spacingXL
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: settingsColumn
|
||||||
|
|
||||||
|
topPadding: 4
|
||||||
|
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
id: warningBox
|
||||||
|
width: parent.width
|
||||||
|
height: warningContent.implicitHeight + Theme.spacingL * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
|
||||||
|
readonly property bool showSetup: !root.inputIncludeStatus.included
|
||||||
|
|
||||||
|
color: showSetup ? Theme.withAlpha(Theme.primary, 0.15) : Theme.withAlpha(Theme.primary, 0)
|
||||||
|
border.color: showSetup ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.primary, 0)
|
||||||
|
border.width: 1
|
||||||
|
visible: showSetup && !root.checkingInclude && CompositorService.isNiri
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: warningContent
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "warning"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("First Time Setup")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.primary
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg("dms/input")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
id: fixButton
|
||||||
|
visible: warningBox.showSetup
|
||||||
|
text: root.fixingInclude ? I18n.tr("Setting up...") : I18n.tr("Setup")
|
||||||
|
backgroundColor: Theme.primary
|
||||||
|
textColor: Theme.primaryText
|
||||||
|
enabled: !root.fixingInclude
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: root.fixInputInclude()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
tags: ["mouse", "input", "sensitivity", "acceleration", "pointer"]
|
||||||
|
title: I18n.tr("Mouse Settings")
|
||||||
|
settingKey: "mouseSettings"
|
||||||
|
iconName: "mouse"
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tags: ["mouse", "left", "handed", "button", "swap"]
|
||||||
|
settingKey: "mouseLeftHanded"
|
||||||
|
text: I18n.tr("Left-Handed Mode")
|
||||||
|
description: I18n.tr("Swap primary and secondary mouse buttons")
|
||||||
|
checked: SettingsData.mouseLeftHanded
|
||||||
|
onToggled: checked => SettingsData.set("mouseLeftHanded", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsSliderRow {
|
||||||
|
tags: ["mouse", "sensitivity", "speed", "accel"]
|
||||||
|
settingKey: "mouseAccelSpeed"
|
||||||
|
text: I18n.tr("Pointer Speed")
|
||||||
|
description: I18n.tr("Adjust pointer sensitivity speed")
|
||||||
|
value: Math.round(SettingsData.mouseAccelSpeed * 10)
|
||||||
|
minimum: -10
|
||||||
|
maximum: 10
|
||||||
|
step: 1
|
||||||
|
defaultValue: 0
|
||||||
|
onSliderValueChanged: newValue => SettingsData.set("mouseAccelSpeed", newValue / 10.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButtonGroupRow {
|
||||||
|
tags: ["mouse", "acceleration", "profile"]
|
||||||
|
settingKey: "mouseAccelProfile"
|
||||||
|
text: I18n.tr("Acceleration Profile")
|
||||||
|
description: I18n.tr("Flat uses constant speed; Adaptive scales with movement speed")
|
||||||
|
model: [I18n.tr("Default"), I18n.tr("Flat"), I18n.tr("Adaptive")]
|
||||||
|
currentIndex: {
|
||||||
|
if (SettingsData.mouseAccelProfile === "flat") return 1;
|
||||||
|
if (SettingsData.mouseAccelProfile === "adaptive") return 2;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
onSelectionChanged: (index, selected) => {
|
||||||
|
if (!selected) return;
|
||||||
|
const profiles = ["default", "flat", "adaptive"];
|
||||||
|
SettingsData.set("mouseAccelProfile", profiles[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tags: ["mouse", "natural", "scroll", "direction"]
|
||||||
|
settingKey: "mouseNaturalScroll"
|
||||||
|
text: I18n.tr("Natural Scrolling")
|
||||||
|
description: I18n.tr("Reverse mouse wheel scrolling direction")
|
||||||
|
checked: SettingsData.mouseNaturalScroll
|
||||||
|
onToggled: checked => SettingsData.set("mouseNaturalScroll", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsSliderRow {
|
||||||
|
tags: ["mouse", "scroll", "speed", "factor"]
|
||||||
|
settingKey: "mouseScrollFactor"
|
||||||
|
text: I18n.tr("Scrolling Speed")
|
||||||
|
description: I18n.tr("Adjust scrolling sensitivity multiplier")
|
||||||
|
value: Math.round(SettingsData.mouseScrollFactor * 10)
|
||||||
|
minimum: 1
|
||||||
|
maximum: 30
|
||||||
|
step: 1
|
||||||
|
defaultValue: 10
|
||||||
|
onSliderValueChanged: newValue => SettingsData.set("mouseScrollFactor", newValue / 10.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButtonGroupRow {
|
||||||
|
tags: ["mouse", "scroll", "method"]
|
||||||
|
settingKey: "mouseScrollMethod"
|
||||||
|
text: I18n.tr("Scroll Method")
|
||||||
|
description: I18n.tr("Choose when to generate scrolling events")
|
||||||
|
model: [I18n.tr("Default"), I18n.tr("No Scroll"), I18n.tr("On Button Down")]
|
||||||
|
currentIndex: {
|
||||||
|
if (SettingsData.mouseScrollMethod === "no-scroll") return 1;
|
||||||
|
if (SettingsData.mouseScrollMethod === "on-button-down") return 2;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
onSelectionChanged: (index, selected) => {
|
||||||
|
if (!selected) return;
|
||||||
|
const methods = ["default", "no-scroll", "on-button-down"];
|
||||||
|
SettingsData.set("mouseScrollMethod", methods[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tags: ["mouse", "middle", "click", "emulation"]
|
||||||
|
settingKey: "mouseMiddleEmulation"
|
||||||
|
text: I18n.tr("Middle Click Emulation")
|
||||||
|
description: I18n.tr("Emulate middle click by pressing left and right buttons")
|
||||||
|
checked: SettingsData.mouseMiddleEmulation
|
||||||
|
onToggled: checked => SettingsData.set("mouseMiddleEmulation", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
tags: ["touchpad", "input", "sensitivity", "tap", "click", "natural"]
|
||||||
|
title: I18n.tr("Touchpad Settings")
|
||||||
|
settingKey: "touchpadSettings"
|
||||||
|
iconName: "trackpad_input_2"
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tags: ["touchpad", "tap", "click"]
|
||||||
|
settingKey: "touchpadTapToClick"
|
||||||
|
text: I18n.tr("Tap to Click")
|
||||||
|
description: I18n.tr("Tap the touchpad surface to trigger left click clicks")
|
||||||
|
checked: SettingsData.touchpadTapToClick
|
||||||
|
onToggled: checked => SettingsData.set("touchpadTapToClick", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tags: ["touchpad", "tap", "drag"]
|
||||||
|
settingKey: "touchpadTapAndDrag"
|
||||||
|
text: I18n.tr("Tap and Drag")
|
||||||
|
description: I18n.tr("Tap and drag on the touchpad to move items")
|
||||||
|
checked: SettingsData.touchpadTapAndDrag
|
||||||
|
onToggled: checked => SettingsData.set("touchpadTapAndDrag", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tags: ["touchpad", "drag", "lock"]
|
||||||
|
settingKey: "touchpadDragLock"
|
||||||
|
text: I18n.tr("Drag Lock")
|
||||||
|
description: I18n.tr("Keep dragging when finger is briefly lifted")
|
||||||
|
checked: SettingsData.touchpadDragLock
|
||||||
|
onToggled: checked => SettingsData.set("touchpadDragLock", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsSliderRow {
|
||||||
|
tags: ["touchpad", "sensitivity", "speed", "accel"]
|
||||||
|
settingKey: "touchpadAccelSpeed"
|
||||||
|
text: I18n.tr("Touchpad Speed")
|
||||||
|
description: I18n.tr("Adjust touchpad pointer speed")
|
||||||
|
value: Math.round(SettingsData.touchpadAccelSpeed * 10)
|
||||||
|
minimum: -10
|
||||||
|
maximum: 10
|
||||||
|
step: 1
|
||||||
|
defaultValue: 0
|
||||||
|
onSliderValueChanged: newValue => SettingsData.set("touchpadAccelSpeed", newValue / 10.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButtonGroupRow {
|
||||||
|
tags: ["touchpad", "acceleration", "profile"]
|
||||||
|
settingKey: "touchpadAccelProfile"
|
||||||
|
text: I18n.tr("Acceleration Profile")
|
||||||
|
description: I18n.tr("Flat uses constant speed; Adaptive scales with movement speed")
|
||||||
|
model: [I18n.tr("Default"), I18n.tr("Flat"), I18n.tr("Adaptive")]
|
||||||
|
currentIndex: {
|
||||||
|
if (SettingsData.touchpadAccelProfile === "flat") return 1;
|
||||||
|
if (SettingsData.touchpadAccelProfile === "adaptive") return 2;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
onSelectionChanged: (index, selected) => {
|
||||||
|
if (!selected) return;
|
||||||
|
const profiles = ["default", "flat", "adaptive"];
|
||||||
|
SettingsData.set("touchpadAccelProfile", profiles[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tags: ["touchpad", "natural", "scroll", "direction"]
|
||||||
|
settingKey: "touchpadNaturalScroll"
|
||||||
|
text: I18n.tr("Natural Scrolling")
|
||||||
|
description: I18n.tr("Reverse two-finger scrolling direction")
|
||||||
|
checked: SettingsData.touchpadNaturalScroll
|
||||||
|
onToggled: checked => SettingsData.set("touchpadNaturalScroll", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsSliderRow {
|
||||||
|
tags: ["touchpad", "scroll", "speed", "factor"]
|
||||||
|
settingKey: "touchpadScrollFactor"
|
||||||
|
text: I18n.tr("Scrolling Speed")
|
||||||
|
description: I18n.tr("Adjust scrolling sensitivity multiplier")
|
||||||
|
value: Math.round(SettingsData.touchpadScrollFactor * 10)
|
||||||
|
minimum: 1
|
||||||
|
maximum: 30
|
||||||
|
step: 1
|
||||||
|
defaultValue: 10
|
||||||
|
onSliderValueChanged: newValue => SettingsData.set("touchpadScrollFactor", newValue / 10.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButtonGroupRow {
|
||||||
|
tags: ["touchpad", "scroll", "method"]
|
||||||
|
settingKey: "touchpadScrollMethod"
|
||||||
|
text: I18n.tr("Scroll Method")
|
||||||
|
description: I18n.tr("Choose when to generate scrolling events")
|
||||||
|
model: [I18n.tr("Default"), I18n.tr("Two Finger"), I18n.tr("Edge"), I18n.tr("No Scroll"), I18n.tr("On Button Down")]
|
||||||
|
currentIndex: {
|
||||||
|
if (SettingsData.touchpadScrollMethod === "two-finger") return 1;
|
||||||
|
if (SettingsData.touchpadScrollMethod === "edge") return 2;
|
||||||
|
if (SettingsData.touchpadScrollMethod === "no-scroll") return 3;
|
||||||
|
if (SettingsData.touchpadScrollMethod === "on-button-down") return 4;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
onSelectionChanged: (index, selected) => {
|
||||||
|
if (!selected) return;
|
||||||
|
const methods = ["default", "two-finger", "edge", "no-scroll", "on-button-down"];
|
||||||
|
SettingsData.set("touchpadScrollMethod", methods[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tags: ["touchpad", "disable", "typing", "dwt"]
|
||||||
|
settingKey: "touchpadDisableWhileTyping"
|
||||||
|
text: I18n.tr("Disable While Typing")
|
||||||
|
description: I18n.tr("Prevent accidental cursor jumps while typing")
|
||||||
|
checked: SettingsData.touchpadDisableWhileTyping
|
||||||
|
onToggled: checked => SettingsData.set("touchpadDisableWhileTyping", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tags: ["touchpad", "disable", "external", "mouse"]
|
||||||
|
settingKey: "touchpadDisableOnExternalMouse"
|
||||||
|
text: I18n.tr("Disable on External Mouse")
|
||||||
|
description: I18n.tr("Disable touchpad when an external mouse is connected")
|
||||||
|
checked: SettingsData.touchpadDisableOnExternalMouse
|
||||||
|
onToggled: checked => SettingsData.set("touchpadDisableOnExternalMouse", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
tags: ["touchpad", "middle", "click", "emulation"]
|
||||||
|
settingKey: "touchpadMiddleEmulation"
|
||||||
|
text: I18n.tr("Middle Click Emulation")
|
||||||
|
description: I18n.tr("Emulate middle click by pressing left and right buttons")
|
||||||
|
checked: SettingsData.touchpadMiddleEmulation
|
||||||
|
onToggled: checked => SettingsData.set("touchpadMiddleEmulation", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,6 +73,7 @@ Singleton {
|
|||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
fetchOutputs();
|
fetchOutputs();
|
||||||
Paths.mkdir(screenshotsDir);
|
Paths.mkdir(screenshotsDir);
|
||||||
|
generateNiriInputConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
@@ -105,6 +106,15 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: CompositorService
|
||||||
|
function onIsNiriChanged() {
|
||||||
|
if (CompositorService.isNiri) {
|
||||||
|
generateNiriInputConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: validateProcess
|
id: validateProcess
|
||||||
command: ["niri", "validate"]
|
command: ["niri", "validate"]
|
||||||
@@ -175,6 +185,20 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: writeInputProcess
|
||||||
|
property string inputContent: ""
|
||||||
|
property string inputPath: ""
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
log.info("Generated input config at", inputPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.warn("Failed to write input config, exit code:", exitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: writeCursorProcess
|
id: writeCursorProcess
|
||||||
property string cursorContent: ""
|
property string cursorContent: ""
|
||||||
@@ -1261,7 +1285,7 @@ window-rule {
|
|||||||
writeAlttabProcess.running = true;
|
writeAlttabProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const name of ["outputs", "binds", "cursor", "windowrules", "colors", "alttab", "layout"]) {
|
for (const name of ["outputs", "binds", "cursor", "windowrules", "colors", "alttab", "layout", "input"]) {
|
||||||
const path = niriDmsDir + "/" + name + ".kdl";
|
const path = niriDmsDir + "/" + name + ".kdl";
|
||||||
Proc.runCommand("niri-ensure-" + name, ["sh", "-c", `mkdir -p "${niriDmsDir}" && [ ! -f "${path}" ] && touch "${path}" || true`], (output, exitCode) => {
|
Proc.runCommand("niri-ensure-" + name, ["sh", "-c", `mkdir -p "${niriDmsDir}" && [ ! -f "${path}" ] && touch "${path}" || true`], (output, exitCode) => {
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0)
|
||||||
@@ -1641,6 +1665,129 @@ window-rule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateNiriInputConfig() {
|
||||||
|
if (!CompositorService.isNiri)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const defaultMouseLeftHanded = false;
|
||||||
|
const defaultMouseMiddleEmulation = false;
|
||||||
|
const defaultMouseNaturalScroll = false;
|
||||||
|
const defaultMouseProfile = "default";
|
||||||
|
const defaultMouseScrollFactor = 1.0;
|
||||||
|
const defaultMouseScrollMethod = "default";
|
||||||
|
const defaultMouseSpeed = 0.0;
|
||||||
|
|
||||||
|
const defaultTouchpadDisableOnExternalMouse = false;
|
||||||
|
const defaultTouchpadDisableWhileTyping = true;
|
||||||
|
const defaultTouchpadDragLock = false;
|
||||||
|
const defaultTouchpadMiddleEmulation = false;
|
||||||
|
const defaultTouchpadNaturalScroll = true;
|
||||||
|
const defaultTouchpadProfile = "default";
|
||||||
|
const defaultTouchpadScrollFactor = 1.0;
|
||||||
|
const defaultTouchpadScrollMethod = "default";
|
||||||
|
const defaultTouchpadSpeed = 0.0;
|
||||||
|
const defaultTouchpadTap = true;
|
||||||
|
const defaultTouchpadTapAndDrag = true;
|
||||||
|
|
||||||
|
const mouseLeftHanded = typeof SettingsData !== "undefined" ? SettingsData.mouseLeftHanded : defaultMouseLeftHanded;
|
||||||
|
const mouseMiddleEmulation = typeof SettingsData !== "undefined" ? SettingsData.mouseMiddleEmulation : defaultMouseMiddleEmulation;
|
||||||
|
const mouseNaturalScroll = typeof SettingsData !== "undefined" ? SettingsData.mouseNaturalScroll : defaultMouseNaturalScroll;
|
||||||
|
const mouseProfile = typeof SettingsData !== "undefined" ? SettingsData.mouseAccelProfile : defaultMouseProfile;
|
||||||
|
const mouseScrollFactor = typeof SettingsData !== "undefined" ? SettingsData.mouseScrollFactor : defaultMouseScrollFactor;
|
||||||
|
const mouseScrollMethod = typeof SettingsData !== "undefined" ? SettingsData.mouseScrollMethod : defaultMouseScrollMethod;
|
||||||
|
const mouseSpeed = typeof SettingsData !== "undefined" ? SettingsData.mouseAccelSpeed : defaultMouseSpeed;
|
||||||
|
|
||||||
|
const touchpadDisableOnExternalMouse = typeof SettingsData !== "undefined" ? SettingsData.touchpadDisableOnExternalMouse : defaultTouchpadDisableOnExternalMouse;
|
||||||
|
const touchpadDisableWhileTyping = typeof SettingsData !== "undefined" ? SettingsData.touchpadDisableWhileTyping : defaultTouchpadDisableWhileTyping;
|
||||||
|
const touchpadDragLock = typeof SettingsData !== "undefined" ? SettingsData.touchpadDragLock : defaultTouchpadDragLock;
|
||||||
|
const touchpadMiddleEmulation = typeof SettingsData !== "undefined" ? SettingsData.touchpadMiddleEmulation : defaultTouchpadMiddleEmulation;
|
||||||
|
const touchpadNaturalScroll = typeof SettingsData !== "undefined" ? SettingsData.touchpadNaturalScroll : defaultTouchpadNaturalScroll;
|
||||||
|
const touchpadProfile = typeof SettingsData !== "undefined" ? SettingsData.touchpadAccelProfile : defaultTouchpadProfile;
|
||||||
|
const touchpadScrollFactor = typeof SettingsData !== "undefined" ? SettingsData.touchpadScrollFactor : defaultTouchpadScrollFactor;
|
||||||
|
const touchpadScrollMethod = typeof SettingsData !== "undefined" ? SettingsData.touchpadScrollMethod : defaultTouchpadScrollMethod;
|
||||||
|
const touchpadSpeed = typeof SettingsData !== "undefined" ? SettingsData.touchpadAccelSpeed : defaultTouchpadSpeed;
|
||||||
|
const touchpadTap = typeof SettingsData !== "undefined" ? SettingsData.touchpadTapToClick : defaultTouchpadTap;
|
||||||
|
const touchpadTapAndDrag = typeof SettingsData !== "undefined" ? SettingsData.touchpadTapAndDrag : defaultTouchpadTapAndDrag;
|
||||||
|
|
||||||
|
const dmsWarning = `// ! DO NOT EDIT !
|
||||||
|
// ! AUTO-GENERATED BY DMS !
|
||||||
|
// ! CHANGES WILL BE OVERWRITTEN !
|
||||||
|
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
let inputContent = dmsWarning + "input {\n";
|
||||||
|
|
||||||
|
// Mouse block
|
||||||
|
inputContent += " mouse {\n";
|
||||||
|
inputContent += ` accel-speed ${mouseSpeed.toFixed(1)}\n`;
|
||||||
|
if (mouseProfile !== "default") {
|
||||||
|
inputContent += ` accel-profile "${mouseProfile}"\n`;
|
||||||
|
}
|
||||||
|
if (mouseNaturalScroll) {
|
||||||
|
inputContent += " natural-scroll\n";
|
||||||
|
}
|
||||||
|
if (mouseLeftHanded) {
|
||||||
|
inputContent += " left-handed\n";
|
||||||
|
}
|
||||||
|
if (mouseMiddleEmulation) {
|
||||||
|
inputContent += " middle-emulation\n";
|
||||||
|
}
|
||||||
|
if (mouseScrollFactor !== 1.0) {
|
||||||
|
inputContent += ` scroll-factor ${mouseScrollFactor.toFixed(1)}\n`;
|
||||||
|
}
|
||||||
|
if (mouseScrollMethod !== "default") {
|
||||||
|
inputContent += ` scroll-method "${mouseScrollMethod}"\n`;
|
||||||
|
}
|
||||||
|
inputContent += " }\n";
|
||||||
|
|
||||||
|
// Touchpad block
|
||||||
|
inputContent += " touchpad {\n";
|
||||||
|
if (touchpadTap) {
|
||||||
|
inputContent += " tap\n";
|
||||||
|
}
|
||||||
|
inputContent += ` accel-speed ${touchpadSpeed.toFixed(1)}\n`;
|
||||||
|
if (touchpadProfile !== "default") {
|
||||||
|
inputContent += ` accel-profile "${touchpadProfile}"\n`;
|
||||||
|
}
|
||||||
|
if (touchpadNaturalScroll) {
|
||||||
|
inputContent += " natural-scroll\n";
|
||||||
|
}
|
||||||
|
if (touchpadDisableOnExternalMouse) {
|
||||||
|
inputContent += " disabled-on-external-mouse\n";
|
||||||
|
}
|
||||||
|
if (touchpadDisableWhileTyping) {
|
||||||
|
inputContent += " dwt\n";
|
||||||
|
}
|
||||||
|
if (touchpadMiddleEmulation) {
|
||||||
|
inputContent += " middle-emulation\n";
|
||||||
|
}
|
||||||
|
if (touchpadScrollFactor !== 1.0) {
|
||||||
|
inputContent += ` scroll-factor ${touchpadScrollFactor.toFixed(1)}\n`;
|
||||||
|
}
|
||||||
|
if (touchpadScrollMethod !== "default") {
|
||||||
|
inputContent += ` scroll-method "${touchpadScrollMethod}"\n`;
|
||||||
|
}
|
||||||
|
if (!touchpadTapAndDrag) {
|
||||||
|
inputContent += " drag false\n";
|
||||||
|
}
|
||||||
|
if (touchpadDragLock) {
|
||||||
|
inputContent += " drag-lock\n";
|
||||||
|
}
|
||||||
|
inputContent += " }\n";
|
||||||
|
|
||||||
|
inputContent += "}\n";
|
||||||
|
|
||||||
|
const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
|
||||||
|
const niriDmsDir = configDir + "/niri/dms";
|
||||||
|
const inputPath = niriDmsDir + "/input.kdl";
|
||||||
|
|
||||||
|
writeInputProcess.inputContent = inputContent;
|
||||||
|
writeInputProcess.inputPath = inputPath;
|
||||||
|
writeInputProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cat > "${inputPath}" << 'EOF'\n${inputContent}\nEOF`];
|
||||||
|
writeInputProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function screenshot(): string {
|
function screenshot(): string {
|
||||||
if (!CompositorService.isNiri) {
|
if (!CompositorService.isNiri) {
|
||||||
|
|||||||
@@ -9916,5 +9916,18 @@
|
|||||||
"widgets"
|
"widgets"
|
||||||
],
|
],
|
||||||
"icon": "dashboard"
|
"icon": "dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "_tab_44",
|
||||||
|
"label": "Mouse & Touchpad",
|
||||||
|
"tabIndex": 44,
|
||||||
|
"category": "Settings",
|
||||||
|
"keywords": [
|
||||||
|
"mouse",
|
||||||
|
"settings",
|
||||||
|
"touchpad"
|
||||||
|
],
|
||||||
|
"icon": "mouse",
|
||||||
|
"conditionKey": "isNiri"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user