Warning, /plasma/plasma-desktop/kcms/keys/ui/main.qml is written in an unsupported language. File is not indexed.

0001 /*
0002     SPDX-FileCopyrightText: 2020 David Redondo <david@david-redondo.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 import QtCore
0008 import QtQuick
0009 import QtQuick.Dialogs
0010 import QtQuick.Layouts
0011 import QtQuick.Controls as QQC2
0012 import QtQml
0013 import QtQml.Models
0014 
0015 import org.kde.kirigami as Kirigami
0016 import org.kde.kcmutils as KCM
0017 import org.kde.private.kcms.keys as Private
0018 
0019 KCM.AbstractKCM {
0020     id: root
0021     implicitWidth: Kirigami.Units.gridUnit * 44
0022     implicitHeight: Kirigami.Units.gridUnit * 33
0023 
0024     framedView: false
0025 
0026     // order must be in sync with ComponentType enum in basemodel.h
0027     readonly property var sectionNames: [i18n("Applications"), i18n("Commands"), i18n("System Services"), i18n("Common Actions")]
0028 
0029     property alias exportActive: exportInfo.visible
0030     readonly property bool errorOccured: kcm.lastError !== ""
0031 
0032     Connections {
0033         target: kcm
0034         function onShowComponent(row) {
0035             components.currentIndex = row
0036         }
0037     }
0038 
0039     actions: [
0040         Kirigami.Action {
0041             enabled: !exportActive
0042             icon.name: "document-import-symbolic"
0043             text: i18nc("@action: button Import shortcut scheme", "Import…")
0044             onTriggered: importSheet.open()
0045         }, Kirigami.Action {
0046             icon.name: exportActive ? "dialog-cancel-symbolic" : "document-export-symbolic"
0047             text: exportActive
0048                   ? i18nc("@action:button", "Cancel Export")
0049                   : i18nc("@action:button Export shortcut scheme", "Export…")
0050             onTriggered: {
0051                 if (exportActive) {
0052                     exportActive = false
0053                 } else if (kcm.needsSave) {
0054                     exportWarning.visible = true
0055                 } else {
0056                     search.text = ""
0057                     exportActive = true
0058                 }
0059             }
0060         }
0061     ]
0062 
0063     header: ColumnLayout {
0064         spacing: Kirigami.Units.smallSpacing
0065 
0066         Kirigami.InlineMessage {
0067             Layout.fillWidth: true
0068             visible: errorOccured
0069             text: kcm.lastError
0070             type: Kirigami.MessageType.Error
0071         }
0072         Kirigami.InlineMessage {
0073             id: exportWarning
0074             Layout.fillWidth: true
0075             text: i18n("Cannot export scheme while there are unsaved changes")
0076             type: Kirigami.MessageType.Warning
0077             showCloseButton: true
0078             Binding on visible {
0079                 when: exportWarning.visible
0080                 value: kcm.needsSave
0081                 restoreMode: Binding.RestoreNone
0082             }
0083         }
0084         Kirigami.InlineMessage {
0085             id: exportInfo
0086             Layout.fillWidth: true
0087             text: i18n("Select the components below that should be included in the exported scheme")
0088             type: Kirigami.MessageType.Information
0089             showCloseButton: true
0090             actions: [
0091                 Kirigami.Action {
0092                     icon.name: "document-save"
0093                     text: i18nc("@action:button Save shortcut scheme", "Save Scheme")
0094                     onTriggered: {
0095                         shortcutSchemeFileDialogLoader.save = true
0096                         shortcutSchemeFileDialogLoader.active = true
0097                         exportActive = false
0098                     }
0099                 }
0100             ]
0101         }
0102         Kirigami.SearchField  {
0103             id: search
0104             enabled: !errorOccured && !exportActive
0105             Layout.fillWidth: true
0106             Binding {
0107                 target: kcm.filteredModel
0108                 property: "filter"
0109                 value: search.text
0110                 restoreMode: Binding.RestoreBinding
0111             }
0112         }
0113     }
0114 
0115     // Since we disabled the scroll views' frame and background, we're responsible
0116     // for setting the background color ourselves, because the background color
0117     // of the page it sits on top of doesn't have the right color for these views.
0118     Rectangle {
0119         anchors.fill: parent
0120         Kirigami.Theme.inherit: false
0121         Kirigami.Theme.colorSet: Kirigami.Theme.View
0122         color: Kirigami.Theme.backgroundColor
0123 
0124         RowLayout  {
0125             anchors.fill: parent
0126             enabled: !errorOccured
0127             spacing: 0
0128 
0129             QQC2.ScrollView {
0130                 id: categoryList
0131 
0132                 Layout.preferredWidth: Kirigami.Units.gridUnit * 14
0133                 Layout.fillHeight: true
0134                 clip: true
0135 
0136                 ListView {
0137                     id: components
0138                     clip: true
0139                     model: kcm.filteredModel
0140                     activeFocusOnTab: true
0141                     add: Transition {
0142                         id: transition
0143                         PropertyAction {
0144                             target: components
0145                             property: "currentIndex"
0146                             value: transition.ViewTransition.index
0147                         }
0148                     }
0149 
0150                     headerPositioning: ListView.OverlayHeader
0151                     header: Kirigami.InlineViewHeader {
0152                         width: ListView.view.width
0153                         Kirigami.ActionToolBar {
0154                             alignment: Qt.AlignRight
0155                             enabled: !exportActive
0156 
0157                             actions: Kirigami.Action {
0158                                 text: i18nc("@action:button Add new shortcut", "Add New")
0159                                 icon.name: "list-add-symbolic"
0160                                 displayHint: Kirigami.DisplayHint.KeepVisible
0161 
0162                                 Kirigami.Action {
0163                                     icon.name: "applications-all-symbolic"
0164                                     text: i18nc("@action:menu End of the sentence 'Add New Application…'", "Application…")
0165                                     onTriggered: kcm.addApplication(root)
0166                                 }
0167 
0168                                 Kirigami.Action {
0169                                     icon.name: "scriptnew-symbolic"
0170                                     text: i18nc("@action:menu End of the sentence 'Add New Command or Script…'", "Command or Script…")
0171                                     onTriggered: addCommandDialog.open()
0172                                 }
0173                             }
0174                         }
0175                     }
0176 
0177                     delegate: QQC2.ItemDelegate {
0178                         id: componentDelegate
0179                         width: ListView.view.width
0180 
0181                         KeyNavigation.right: shortcutsList
0182 
0183                         onClicked: ListView.view.currentIndex = index
0184                         highlighted: ListView.isCurrentItem
0185 
0186                         contentItem: RowLayout {
0187                             spacing: Kirigami.Units.smallSpacing
0188                             Kirigami.IconTitleSubtitle {
0189                                 id: label
0190 
0191                                 icon.name: model.decoration
0192                                 title: model.display
0193                                 Layout.fillWidth: true
0194                                 opacity: model.pendingDeletion ? 0.5 : 1.0
0195                                 selected: componentDelegate.highlighted || componentDelegate.down
0196                             }
0197 
0198                             QQC2.CheckBox {
0199                                 checked: model.checked
0200                                 visible: exportActive
0201                                 onToggled: model.checked = checked
0202                             }
0203                             QQC2.Button {
0204                                 id: editButton
0205 
0206                                 implicitHeight: label.implicitHeight
0207                                 implicitWidth: implicitHeight
0208 
0209                                 visible: model.section === Private.ComponentType.Command
0210                                          && !exportActive
0211                                          && !model.pendingDeletion
0212                                          && (componentDelegate.hovered || componentDelegate.ListView.isCurrentItem)
0213                                 icon.name: "edit-rename"
0214                                 onClicked: {
0215                                     addCommandDialog.editing = true;
0216                                     addCommandDialog.componentName = model.component;
0217                                     // for commands, Name == Exec
0218                                     addCommandDialog.oldExec = model.display;
0219                                     addCommandDialog.commandListItemDelegate = componentDelegate;
0220                                     addCommandDialog.open();
0221                                 }
0222                                 QQC2.ToolTip {
0223                                     text: i18nc("@tooltip:button %1 is the text of a custom command", "Edit command for %1", model.display)
0224                                 }
0225                             }
0226                             QQC2.Button {
0227                                 id: deleteButton
0228 
0229                                 implicitHeight: label.implicitHeight
0230                                 implicitWidth: implicitHeight
0231 
0232                                 visible: model.section !== Private.ComponentType.CommonAction
0233                                          && !exportActive
0234                                          && !model.pendingDeletion
0235                                          && (componentDelegate.hovered || componentDelegate.ListView.isCurrentItem)
0236                                 icon.name: "edit-delete"
0237                                 onClicked: model.pendingDeletion = true
0238                                 QQC2.ToolTip {
0239                                     text: i18n("Remove all shortcuts for %1", model.display)
0240                                 }
0241                             }
0242                             QQC2.Button {
0243                                 implicitHeight: label.implicitHeight
0244                                 implicitWidth: implicitHeight
0245 
0246                                 visible: !exportActive && model.pendingDeletion
0247                                 icon.name: "edit-undo"
0248                                 onClicked: model.pendingDeletion = false
0249                                 QQC2.ToolTip {
0250                                     text: i18n("Undo deletion")
0251                                 }
0252                             }
0253                             Rectangle {
0254                                 id: defaultIndicator
0255                                 radius: width * 0.5
0256                                 implicitWidth: Kirigami.Units.largeSpacing
0257                                 implicitHeight: Kirigami.Units.largeSpacing
0258                                 visible: kcm.defaultsIndicatorsVisible
0259                                 opacity: !model.isDefault
0260                                 color: Kirigami.Theme.neutralTextColor
0261                             }
0262                         }
0263                     }
0264 
0265                     section.property: "section"
0266                     section.delegate: Kirigami.ListSectionHeader {
0267                         label: root.sectionNames[section]
0268                         width:  components.width
0269                         QQC2.CheckBox {
0270                             id: sectionCheckbox
0271                             Layout.alignment: Qt.AlignRight
0272                             // width of indicator + layout spacing
0273                             Layout.rightMargin: kcm.defaultsIndicatorsVisible ? Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0
0274                             visible: exportActive
0275                             onToggled: {
0276                                 const checked = sectionCheckbox.checked
0277                                 const startIndex = kcm.shortcutsModel.index(0, 0)
0278                                 const indices = kcm.shortcutsModel.match(startIndex, Private.BaseModel.SectionRole, section, -1, Qt.MatchExactly)
0279                                 for (const index of indices) {
0280                                     kcm.shortcutsModel.setData(index, checked, Private.BaseModel.CheckedRole)
0281                                 }
0282                             }
0283                             Connections {
0284                                 enabled: exportActive
0285                                 target: kcm.shortcutsModel
0286                                 function onDataChanged (topLeft, bottomRight, roles) {
0287                                     const startIndex = kcm.shortcutsModel.index(0, 0)
0288                                     const indices = kcm.shortcutsModel.match(startIndex, Private.BaseModel.SectionRole, section, -1, Qt.MatchExactly)
0289                                     sectionCheckbox.checked = indices.reduce((acc, index) => acc && kcm.shortcutsModel.data(index, Private.BaseModel.CheckedRole), true)
0290                                 }
0291                             }
0292                         }
0293                     }
0294 
0295                     onCurrentItemChanged: dm.rootIndex = kcm.filteredModel.index(currentIndex, 0)
0296                     onCurrentIndexChanged: {
0297                         shortcutsList.selectedIndex = -1;
0298                     }
0299 
0300                     Kirigami.PlaceholderMessage {
0301                         anchors.centerIn: parent
0302                         width: parent.width - (Kirigami.Units.largeSpacing * 4)
0303                         visible: components.count === 0 && search.text.length > 0
0304                         text: i18n("No items matched the search terms")
0305                     }
0306                 }
0307             }
0308 
0309             Kirigami.Separator {
0310                 Layout.fillHeight: true
0311             }
0312 
0313             QQC2.ScrollView  {
0314                 enabled: !exportActive
0315                 id: shortcutsScroll
0316                 Layout.fillHeight: true
0317                 Layout.fillWidth: true
0318                 clip: true
0319 
0320                 ListView {
0321                     clip:true
0322                     id: shortcutsList
0323                     property int selectedIndex: -1
0324                     model: DelegateModel {
0325                         id: dm
0326                         model: rootIndex.valid ?  kcm.filteredModel : undefined
0327                         delegate: ShortcutActionDelegate {
0328                             showExpandButton: shortcutsList.count > 1
0329                         }
0330                         KeyNavigation.left: components
0331                     }
0332 
0333                     Kirigami.PlaceholderMessage {
0334                         anchors.centerIn: parent
0335                         width: parent.width - (Kirigami.Units.largeSpacing * 4)
0336                         visible: components.currentIndex == -1
0337                         text: i18n("Select an item from the list to view its shortcuts here")
0338                     }
0339                 }
0340             }
0341         }
0342     }
0343 
0344     Loader {
0345         id: shortcutSchemeFileDialogLoader
0346         active: false
0347         property bool save
0348         sourceComponent: FileDialog {
0349             title: save ? i18n("Export Shortcut Scheme") : i18n("Import Shortcut Scheme")
0350             currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
0351             nameFilters: [ i18nc("Template for file dialog","Shortcut Scheme (*.kksrc)") ]
0352             defaultSuffix: ".kksrc"
0353             fileMode: shortcutSchemeFileDialogLoader.save ? FileDialog.SaveFile : FileDialog.OpenFile
0354             Component.onCompleted: open()
0355             onAccepted: {
0356                 if (save) {
0357                     kcm.writeScheme(selectedFile)
0358                 } else {
0359                     var schemes = schemeBox.model
0360                     schemes.splice(schemes.length - 1, 0, {name: kcm.urlFilename(selectedFile), url: selectedFile})
0361                     schemeBox.model = schemes
0362                     schemeBox.currentIndex = schemes.length - 2
0363                 }
0364                 shortcutSchemeFileDialogLoader.active = false
0365             }
0366             onRejected: shortcutSchemeFileDialogLoader.active = false
0367         }
0368     }
0369 
0370     Kirigami.PromptDialog {
0371         id: addCommandDialog
0372         property bool editing: false
0373         property string componentName: ""
0374         property string oldExec: ""
0375         property Item commandListItemDelegate: null
0376 
0377         width: Math.max(root.width / 2, Kirigami.Units.gridUnit * 24)
0378 
0379         title: editing ? i18n("Edit Command") : i18n("Add Command")
0380 
0381         onVisibleChanged: {
0382             if (visible) {
0383                 cmdField.clear();
0384                 cmdField.forceActiveFocus();
0385                 if (editing) {
0386                     cmdField.text = oldExec;
0387                 }
0388             }
0389         }
0390         onRejected: {
0391             if (addCommandDialog.editing) {
0392                 addCommandDialog.editing = false;
0393             }
0394         }
0395 
0396         property Kirigami.Action addCommandAction: Kirigami.Action {
0397             text: addCommandDialog.editing ? i18n("Save") : i18n("Add")
0398             icon.name: addCommandDialog.editing ? "dialog-ok" : "list-add"
0399             enabled: cmdField.length > 0
0400             onTriggered: {
0401                 if (addCommandDialog.editing) {
0402                     const newLabel = kcm.editCommand(addCommandDialog.componentName, cmdField.text);
0403                     if (addCommandDialog.commandListItemDelegate) {
0404                         addCommandDialog.commandListItemDelegate.label = newLabel;
0405                     }
0406                 } else {
0407                     kcm.addCommand(cmdField.text);
0408                 }
0409                 addCommandDialog.editing = false;
0410                 addCommandDialog.close();
0411             }
0412         }
0413 
0414         standardButtons: Kirigami.Dialog.NoButton
0415 
0416         customFooterActions: [addCommandAction]
0417 
0418         ColumnLayout {
0419             anchors.centerIn: parent
0420             spacing: Kirigami.Units.smallSpacing
0421 
0422             QQC2.Label {
0423                 text: i18n("Enter a command or choose a script file:")
0424                 textFormat: Text.PlainText
0425             }
0426             RowLayout {
0427                 Layout.fillWidth: true
0428                 spacing: Kirigami.Units.smallSpacing
0429 
0430                 QQC2.TextField {
0431                     id: cmdField
0432                     Layout.fillWidth: true
0433                     font.family: "monospace"
0434                     onAccepted: addCommandDialog.addCommandAction.triggered()
0435                 }
0436                 QQC2.Button {
0437                     icon.name: "document-open"
0438                     text: i18nc("@action:button", "Choose…")
0439                     onClicked: {
0440                         openScriptFileDialogLoader.active = true
0441                     }
0442                 }
0443             }
0444         }
0445     }
0446 
0447     Loader {
0448         id: openScriptFileDialogLoader
0449         active: false
0450         sourceComponent: FileDialog {
0451             title: i18nc("@title:window", "Choose Script File")
0452             currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
0453             nameFilters: [ i18nc("Template for file dialog","Script file (*.*sh)") ]
0454             Component.onCompleted: open()
0455             onAccepted: {
0456                 cmdField.text = selectedFile
0457                 cmdField.text = kcm.quoteUrl(selectedFile)
0458                 openScriptFileDialogLoader.active = false
0459             }
0460             onRejected: openScriptFileDialogLoader.active = false
0461         }
0462     }
0463 
0464     Kirigami.OverlaySheet {
0465         id: importSheet
0466 
0467         title: i18n("Import Shortcut Scheme")
0468 
0469         ColumnLayout {
0470             anchors.centerIn: parent
0471             spacing: Kirigami.Units.smallSpacing
0472 
0473             QQC2.Label {
0474                 text: i18n("Select the scheme to import:")
0475                 textFormat: Text.PlainText
0476                 Layout.margins: Kirigami.Units.largeSpacing
0477             }
0478 
0479             RowLayout {
0480                 spacing: Kirigami.Units.smallSpacing
0481                 Layout.margins: Kirigami.Units.largeSpacing
0482 
0483                 QQC2.ComboBox {
0484                     id: schemeBox
0485                     readonly property bool customSchemeSelected: currentIndex == count - 1
0486                     property string url: ""
0487                     currentIndex: count - 1
0488                     textRole: "name"
0489                     onActivated: url = model[index]["url"]
0490                     Component.onCompleted: {
0491                         var defaultSchemes = kcm.defaultSchemes()
0492                         defaultSchemes.push({name: i18n("Custom Scheme"), url: "unused"})
0493                         model = defaultSchemes
0494                     }
0495                 }
0496                 QQC2.Button {
0497                     text: schemeBox.customSchemeSelected ? i18n("Select File…") : i18n("Import")
0498                     onClicked: {
0499                         if (schemeBox.customSchemeSelected) {
0500                             shortcutSchemeFileDialogLoader.save = false;
0501                             shortcutSchemeFileDialogLoader.active = true;
0502                         } else {
0503                             kcm.loadScheme(schemeBox.model[schemeBox.currentIndex]["url"])
0504                             importSheet.close()
0505                         }
0506                     }
0507                 }
0508             }
0509         }
0510     }
0511 }
0512