Warning, /plasma/plasma-desktop/kcms/mouse/kcm/libinput/main.qml is written in an unsupported language. File is not indexed.

0001 /*
0002     SPDX-FileCopyrightText: 2018 Roman Gilg <subdiff@gmail.com>
0003     SPDX-FileCopyrightText: 2018 Furkan Tokac <furkantokac34@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 import QtQuick
0009 import QtQuick.Controls as QQC2
0010 import QtQuick.Layouts
0011 
0012 import org.kde.kirigami as Kirigami
0013 import org.kde.kquickcontrols
0014 
0015 Kirigami.ApplicationItem {
0016     id: root
0017 
0018     pageStack.globalToolBar.style:  Kirigami.ApplicationHeaderStyle.None
0019     pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn
0020     pageStack.defaultColumnWidth: Kirigami.Units.gridUnit * 20
0021 
0022     property alias deviceIndex: deviceSelector.currentIndex
0023     signal changeSignal()
0024 
0025     property QtObject device
0026     property int deviceCount: backend.deviceCount
0027 
0028     property bool loading: false
0029 
0030     enabled: deviceCount > 0
0031 
0032     function resetModel(index) {
0033         deviceCount = backend.deviceCount
0034         formLayout.enabled = deviceCount
0035         deviceSelector.enabled = deviceCount > 1
0036 
0037         loading = true
0038         if (deviceCount) {
0039             device = deviceModel[index]
0040             deviceSelector.model = deviceModel
0041             deviceSelector.currentIndex = index
0042         } else {
0043             deviceSelector.model = [""]
0044         }
0045         loading = false
0046     }
0047 
0048     function syncValuesFromBackend() {
0049         loading = true
0050 
0051         deviceEnabled.load()
0052         leftHanded.load()
0053         middleEmulation.load()
0054         accelSpeedSpinbox.load()
0055         accelSpeedSlider.load()
0056         accelProfile.load()
0057         naturalScroll.load()
0058         scrollFactor.load()
0059         buttonMappings.load()
0060 
0061         loading = false
0062     }
0063 
0064     pageStack.initialPage: Kirigami.ScrollablePage {
0065         spacing: Kirigami.Units.smallSpacing
0066 
0067         Kirigami.FormLayout {
0068             id: formLayout
0069             enabled: deviceCount
0070 
0071             // Device
0072             QQC2.ComboBox {
0073                 id: deviceSelector
0074                 Kirigami.FormData.label: i18nd("kcmmouse", "Device:")
0075                 enabled: deviceCount > 1
0076                 Layout.fillWidth: true
0077                 model: deviceModel
0078                 textRole: "name"
0079 
0080                 onCurrentIndexChanged: {
0081                     if (deviceCount) {
0082                         device = deviceModel[currentIndex]
0083                         if (!loading) {
0084                             changeSignal()
0085                         }
0086                     }
0087                     root.syncValuesFromBackend()
0088                 }
0089             }
0090 
0091             Item {
0092                 Kirigami.FormData.isSection: false
0093             }
0094 
0095             // General
0096             QQC2.CheckBox {
0097                 id: deviceEnabled
0098                 Kirigami.FormData.label: i18nd("kcmmouse", "General:")
0099                 text: i18nd("kcmmouse", "Device enabled")
0100 
0101                 function load() {
0102                     if (!formLayout.enabled) {
0103                         checked = false
0104                         return
0105                     }
0106                     enabled = device.supportsDisableEvents
0107                     checked = enabled && device.enabled
0108                 }
0109 
0110                 onCheckedChanged: {
0111                     if (enabled && !root.loading) {
0112                         device.enabled = checked
0113                         root.changeSignal()
0114                     }
0115                 }
0116 
0117                 QQC2.ToolTip.delay: 1000
0118                 QQC2.ToolTip.visible: hovered
0119                 QQC2.ToolTip.text: i18nd("kcmmouse", "Accept input through this device.")
0120             }
0121 
0122             QQC2.CheckBox {
0123                 id: leftHanded
0124                 text: i18nd("kcmmouse", "Left handed mode")
0125 
0126                 function load() {
0127                     if (!formLayout.enabled) {
0128                         checked = false
0129                         return
0130                     }
0131                     enabled = device.supportsLeftHanded
0132                     checked = enabled && device.leftHanded
0133                 }
0134 
0135                 onCheckedChanged: {
0136                     if (enabled && !root.loading) {
0137                         device.leftHanded = checked
0138                         root.changeSignal()
0139                     }
0140                 }
0141 
0142                 QQC2.ToolTip.delay: 1000
0143                 QQC2.ToolTip.visible: hovered
0144                 QQC2.ToolTip.text: i18nd("kcmmouse", "Swap left and right buttons.")
0145             }
0146 
0147             QQC2.CheckBox {
0148                 id: middleEmulation
0149                 text: i18nd("kcmmouse", "Press left and right buttons for middle-click")
0150 
0151                 function load() {
0152                     if (!formLayout.enabled) {
0153                         checked = false
0154                         return
0155                     }
0156                     enabled = device.supportsMiddleEmulation
0157                     checked = enabled && device.middleEmulation
0158                 }
0159 
0160                 onCheckedChanged: {
0161                     if (enabled && !root.loading) {
0162                         device.middleEmulation = checked
0163                         root.changeSignal()
0164                     }
0165                 }
0166 
0167                 QQC2.ToolTip.delay: 1000
0168                 QQC2.ToolTip.visible: hovered
0169                 QQC2.ToolTip.text: i18nd("kcmmouse", "Clicking left and right button simultaneously sends middle button click.")
0170             }
0171 
0172             Item {
0173                 Kirigami.FormData.isSection: false
0174             }
0175 
0176             // Acceleration
0177             RowLayout {
0178                 Kirigami.FormData.label: i18nd("kcmmouse", "Pointer speed:")
0179                 id: accelSpeed
0180                 Layout.fillWidth: true
0181 
0182                 function onAccelSpeedChanged(val) {
0183                     // check slider
0184                     if (val != accelSpeedSlider.accelSpeedValue) {
0185                         accelSpeedSlider.accelSpeedValue = val
0186                         accelSpeedSlider.value = Math.round(6 + (val / 100) / 0.2)
0187                     }
0188 
0189                     // check spinbox
0190                     if (val != accelSpeedSpinbox.value) {
0191                         accelSpeedSpinbox.value = val
0192                     }
0193 
0194                     // check libinput accelspeed
0195                     if ((val / 1000) != device.pointerAcceleration) {
0196                         device.pointerAcceleration = val / 100
0197                         root.changeSignal()
0198                     }
0199                 }
0200 
0201                 QQC2.Slider {
0202                     id: accelSpeedSlider
0203                     Layout.fillWidth: true
0204 
0205                     from: 1
0206                     to: 11
0207                     stepSize: 1
0208                     property int accelSpeedValue: 0 // [-100, 100]
0209 
0210                     function load() {
0211                         enabled = device.supportsPointerAcceleration
0212                         if (!enabled) {
0213                             return
0214                         }
0215 
0216                         accelSpeedValue = Math.round(device.pointerAcceleration * 100)
0217 
0218                         // convert libinput pointer acceleration range [-1, 1] to slider range [1, 11]
0219                         value = Math.round(6 + device.pointerAcceleration / 0.2)
0220                     }
0221 
0222                     onValueChanged: {
0223                         if (device != undefined && enabled && !root.loading) {
0224                             // convert slider range [1, 11] to accelSpeedValue range [-100, 100]
0225                             accelSpeedValue = Math.round(((value - 6) * 0.2) * 100)
0226 
0227                             accelSpeed.onAccelSpeedChanged(accelSpeedValue)
0228                         }
0229                     }
0230                 }
0231 
0232                 QQC2.SpinBox {
0233                     id: accelSpeedSpinbox
0234 
0235                     Layout.minimumWidth: Kirigami.Units.gridUnit * 5
0236 
0237                     from: -100
0238                     to: 100
0239                     stepSize: 1
0240                     editable: true
0241 
0242                     validator: DoubleValidator {
0243                         bottom: accelSpeedSpinbox.from
0244                         top: accelSpeedSpinbox.to
0245                     }
0246 
0247                     function load() {
0248                         enabled = device.supportsPointerAcceleration
0249                         if (!enabled) {
0250                             return
0251                         }
0252 
0253                         // if existing configuration or another application set a value with more than 2 decimals
0254                         // we reduce the precision to 2
0255                         value = Math.round(device.pointerAcceleration * 100)
0256                     }
0257 
0258                     onValueChanged: {
0259                         if (device != undefined && enabled && !root.loading) {
0260                             accelSpeed.onAccelSpeedChanged(value)
0261                         }
0262                     }
0263 
0264                     textFromValue: function(val, locale) {
0265                         return Number(val / 100).toLocaleString(locale, "f", 2)
0266                     }
0267 
0268                     valueFromText: function(text, locale) {
0269                         return Number.fromLocaleString(locale, text) * 100
0270                     }
0271                 }
0272             }
0273 
0274             ColumnLayout {
0275                 id: accelProfile
0276                 spacing: Kirigami.Units.smallSpacing
0277                 Kirigami.FormData.label: i18nd("kcmmouse", "Pointer acceleration:")
0278                 Kirigami.FormData.buddyFor: accelProfileFlat
0279 
0280                 function load() {
0281                     enabled = device.supportsPointerAccelerationProfileAdaptive
0282 
0283                     if (!enabled) {
0284                         accelProfileAdaptive.checked = false
0285                         accelProfileFlat.checked = false
0286                         return
0287                     }
0288 
0289                     if(device.pointerAccelerationProfileAdaptive) {
0290                         accelProfileAdaptive.checked = true
0291                         accelProfileFlat.checked = false
0292                     } else {
0293                         accelProfileAdaptive.checked = false
0294                         accelProfileFlat.checked = true
0295                     }
0296                 }
0297 
0298                 function syncCurrent() {
0299                     if (enabled && !root.loading) {
0300                         device.pointerAccelerationProfileFlat = accelProfileFlat.checked
0301                         device.pointerAccelerationProfileAdaptive = accelProfileAdaptive.checked
0302                         root.changeSignal()
0303                     }
0304                 }
0305 
0306                 QQC2.RadioButton {
0307                     id: accelProfileFlat
0308                     text: i18nd("kcmmouse", "None")
0309 
0310                     QQC2.ToolTip.delay: 1000
0311                     QQC2.ToolTip.visible: hovered
0312                     QQC2.ToolTip.text: i18nd("kcmmouse", "Cursor moves the same distance as the mouse movement.")
0313                     onCheckedChanged: accelProfile.syncCurrent()
0314                 }
0315 
0316                 QQC2.RadioButton {
0317                     id: accelProfileAdaptive
0318                     text: i18nd("kcmmouse", "Standard")
0319 
0320                     QQC2.ToolTip.delay: 1000
0321                     QQC2.ToolTip.visible: hovered
0322                     QQC2.ToolTip.text: i18nd("kcmmouse", "Cursor travel distance depends on the mouse movement speed.")
0323                     onCheckedChanged: accelProfile.syncCurrent()
0324                 }
0325             }
0326 
0327             Item {
0328                 Kirigami.FormData.isSection: false
0329             }
0330 
0331             // Scrolling
0332             QQC2.CheckBox {
0333                 id: naturalScroll
0334                 Kirigami.FormData.label: i18nd("kcmmouse", "Scrolling:")
0335                 text: i18nd("kcmmouse", "Invert scroll direction")
0336 
0337                 function load() {
0338                     enabled = device.supportsNaturalScroll
0339                     checked = enabled && device.naturalScroll
0340                 }
0341 
0342                 onCheckedChanged: {
0343                     if (enabled && !root.loading) {
0344                         device.naturalScroll = checked
0345                         root.changeSignal()
0346                     }
0347                 }
0348 
0349                 QQC2.ToolTip.delay: 1000
0350                 QQC2.ToolTip.visible: hovered
0351                 QQC2.ToolTip.text: i18nd("kcmmouse", "Touchscreen like scrolling.")
0352             }
0353 
0354             // Scroll Speed aka scroll Factor
0355             GridLayout {
0356                 Kirigami.FormData.label: i18nd("kcm_touchpad", "Scrolling speed:")
0357                 Kirigami.FormData.buddyFor: scrollFactor
0358                 Layout.fillWidth: true
0359 
0360                 columns: 3
0361 
0362                 QQC2.Slider {
0363                     id: scrollFactor
0364                     Layout.fillWidth: true
0365 
0366                     from: 0
0367                     to: 14
0368                     stepSize: 1
0369 
0370                     readonly property list<real> values: [
0371                         0.1,
0372                         0.3,
0373                         0.5,
0374                         0.75,
0375                         1, // default
0376                         1.5,
0377                         2,
0378                         3,
0379                         4,
0380                         5,
0381                         7,
0382                         9,
0383                         12,
0384                         15,
0385                         20
0386                     ]
0387 
0388                     Layout.columnSpan: 3
0389 
0390                     function load() {
0391                         let index = values.indexOf(device.scrollFactor)
0392                         if (index === -1) {
0393                             index = values.indexOf(1);
0394                         }
0395                         value = index
0396                     }
0397 
0398                     onMoved: {
0399                         device.scrollFactor = values[value]
0400                         root.changeSignal()
0401                     }
0402                 }
0403 
0404                 //row 2
0405                 QQC2.Label {
0406                     text: i18ndc("kcmmouse", "Slower Scroll", "Slower")
0407                     textFormat: Text.PlainText
0408                 }
0409                 Item {
0410                     Layout.fillWidth: true
0411                 }
0412                 QQC2.Label {
0413                     text: i18ndc("kcmmouse", "Faster Scroll Speed", "Faster")
0414                     textFormat: Text.PlainText
0415                 }
0416 
0417             }
0418 
0419             Item {
0420                 Kirigami.FormData.isSection: true
0421             }
0422 
0423             QQC2.Button  {
0424                 text: i18ndc("kcmmouse", "@action:button", "Re-bind Additional Mouse Buttons…")
0425                 visible: buttonMappings.model.length > 0 || Array.prototype.some.call(deviceModel, device => device.supportedButtons & ~(Qt.LeftButton | Qt.RightButton | Qt.MiddleButton))
0426                 onClicked: root.pageStack.push(buttonPage)
0427             }
0428         }
0429     }
0430 
0431     Kirigami.ScrollablePage {
0432         id: buttonPage
0433         visible: false
0434 
0435         MouseArea {
0436             // Deliberately using MouseArea on the page instead of a TapHandler on the button, so we can capture clicks anywhere
0437             id: buttonCapture
0438             property var lastButton: {}
0439 
0440             anchors.fill: parent
0441             enabled: newBinding.checked
0442             preventStealing: true
0443             acceptedButtons: Qt.AllButtons & ~(Qt.LeftButton | Qt.RightButton | Qt.MiddleButton)
0444             onClicked: {
0445                 lastButton = buttonMappings.extraButtons.find(entry => Qt[entry.buttonName] === mouse.button)
0446                 newBinding.visible = false
0447                 newKeySequenceItem.visible = true
0448                 newKeySequenceItem.startCapturing()
0449             }
0450         }
0451 
0452        ColumnLayout {
0453             Kirigami.FormLayout {
0454                 id: buttonLayout
0455                 twinFormLayouts: otherLayout
0456                 Repeater {
0457                     id: buttonMappings
0458 
0459                     readonly property var extraButtons: Array.from({length: 24}, (value, index) => ({
0460                         buttonName: "ExtraButton" + (index + 1),
0461                         button: Qt["ExtraButton" + (index + 1)],
0462                         label: i18ndc("kcmmouse", "@label for assigning an action to a numbered button", "Extra Button %1:", index + 1)
0463                     }))
0464 
0465                     function load() {
0466                         model = Qt.binding(() => extraButtons.filter(entry => backend.buttonMapping.hasOwnProperty(entry.buttonName)))
0467                     }
0468 
0469                     delegate: KeySequenceItem {
0470                         Kirigami.FormData.label: modelData.label
0471 
0472                         keySequence: backend.buttonMapping[modelData.buttonName]
0473 
0474                         modifierlessAllowed: true
0475                         multiKeyShortcutsAllowed: false
0476                         checkForConflictsAgainst: ShortcutType.None
0477 
0478                         onCaptureFinished: {
0479                             const copy = backend.buttonMapping;
0480                             copy[modelData.buttonName] = keySequence
0481                             backend.buttonMapping = copy
0482                             root.changeSignal()
0483                         }
0484                     }
0485                 }
0486             }
0487 
0488             Kirigami.InlineMessage {
0489                 id: explanationLabel
0490                 Layout.fillWidth: true
0491                 visible: newBinding.checked || newKeySequenceItem.visible
0492                 text: newBinding.visible ? i18ndc("kcmmouse","@action:button", "Press the mouse button for which you want to add a key binding") :
0493                     i18ndc("kcmmouse","@action:button, %1 is the translation of 'Extra Button %1' from above", "Enter the new key combination for %1", buttonCapture.lastButton.label)
0494                 actions: [
0495                     Kirigami.Action {
0496                         icon.name: "dialog-cancel"
0497                         text: i18ndc("kcmmouse", "@action:button", "Cancel")
0498                         onTriggered: {
0499                             newKeySequenceItem.visible = false;
0500                             newBinding.visible = true
0501                             newBinding.checked = false
0502                         }
0503                     }
0504                 ]
0505             }
0506 
0507             Kirigami.FormLayout {
0508                 id: otherLayout
0509                 twinFormLayouts: buttonLayout
0510 
0511                 QQC2.Button {
0512                     id: newBinding
0513                     checkable: true
0514                     text: checked ? i18ndc("kcmmouse", "@action:button", "Press a mouse button ") :
0515                         i18ndc("kcmmouse", "@action:button, Bind a mousebutton to keyboard key(s)", "Add Binding…")
0516                     icon.name: "list-add"
0517                 }
0518                 KeySequenceItem {
0519                     id: newKeySequenceItem
0520                     visible: false
0521 
0522                     modifierlessAllowed: true
0523                     multiKeyShortcutsAllowed: false
0524                     checkForConflictsAgainst: ShortcutType.None
0525 
0526                     onCaptureFinished: {
0527                         visible = false
0528                         newBinding.visible = true
0529                         newBinding.checked = false
0530                         const copy = backend.buttonMapping;
0531                         copy[buttonCapture.lastButton.buttonName] = keySequence
0532                         backend.buttonMapping = copy
0533                         root.changeSignal()
0534                     }
0535                 }
0536 
0537 
0538                 Item {
0539                     Kirigami.FormData.isSection: true
0540                 }
0541 
0542                 QQC2.Button  {
0543                     onClicked: root.pageStack.pop()
0544                     text: i18ndc("kcmmouse", "@action:button", "Go back")
0545                     icon.name: "go-previous"
0546                 }
0547             }
0548         }
0549     }
0550 }