/*SPDX-FileCopyrightText: 2020 David Redondo <david@david-redondo.de>SPDX-License-Identifier: GPL-2.0-or-later*/import QtCoreimport QtQuickimport QtQuick.Dialogsimport QtQuick.Layoutsimport QtQuick.Controls as QQC2import QtQmlimport QtQml.Modelsimport org.kde.kirigami as Kirigamiimport org.kde.kcmutils as KCMimport org.kde.private.kcms.keys as PrivateKCM.AbstractKCM {id: rootimplicitWidth: Kirigami.Units.gridUnit * 44implicitHeight: Kirigami.Units.gridUnit * 33framedView: false// order must be in sync with ComponentType enum in basemodel.hreadonly property var sectionNames: [i18nc("@title:group list section name", "Applications"), i18nc("@title:group list section name", "Commands"), i18nc("@title:group list section name", "System Services"), i18nc("@title:group list section name", "Common Actions")]property alias exportActive: exportInfo.visiblereadonly property bool errorOccured: kcm.lastError !== ""Connections {target: kcmfunction onShowComponent(row) {components.currentIndex = row}}actions: [Kirigami.Action {enabled: !exportActiveicon.name: "document-import-symbolic"text: i18nc("@action:button Import shortcut scheme", "Importβ¦")Accessible.name: i18nc("@action:button accessible", "Import shortcut scheme")onTriggered: importSheet.open()}, Kirigami.Action {icon.name: exportActive ? "dialog-cancel-symbolic" : "document-export-symbolic"text: exportActive? i18nc("@action:button", "Cancel Export"): i18nc("@action:button Export shortcut scheme", "Exportβ¦")Accessible.name: exportActive? text: i18nc("@action:button accessible", "Export shortcut scheme")onTriggered: {if (exportActive) {exportActive = false} else if (kcm.needsSave) {exportWarning.visible = true} else {search.text = ""exportActive = true}}},Kirigami.Action {text: i18nc("@action:button Add new shortcut", "Add New")icon.name: "list-add-symbolic"displayHint: Kirigami.DisplayHint.KeepVisibleenabled: !exportActiveKirigami.Action {icon.name: "applications-all-symbolic"text: i18nc("@action:menu End of the sentence 'Add New Applicationβ¦'", "Applicationβ¦")Accessible.name: i18nc("@action:menu accessible", "Add new application")Accessible.role: Accessible.MenuItemonTriggered: kcm.addApplication(root)}Kirigami.Action {icon.name: "scriptnew-symbolic"text: i18nc("@action:menu End of the sentence 'Add New Command or Scriptβ¦'", "Command or Scriptβ¦")Accessible.name: i18nc("@action:menu accessible", "Add new command or script")Accessible.role: Accessible.MenuItemonTriggered: {addCommandDialog.editing = falseaddCommandDialog.open()}}}]headerPaddingEnabled: false // Let the InlineMessage touch the edgesheader: ColumnLayout {spacing: 0Kirigami.InlineMessage {Layout.fillWidth: truevisible: errorOccuredtext: kcm.lastErrorposition: Kirigami.InlineMessage.Position.Headertype: Kirigami.MessageType.Error}Kirigami.InlineMessage {id: exportWarningLayout.fillWidth: truetext: i18nc("@info:status inlinemessage", "Cannot export scheme while there are unsaved changes")position: Kirigami.InlineMessage.Position.Headertype: Kirigami.MessageType.WarningshowCloseButton: trueBinding on visible {when: exportWarning.visiblevalue: kcm.needsSaverestoreMode: Binding.RestoreNone}}Kirigami.InlineMessage {id: exportInfoLayout.fillWidth: truetext: i18nc("@info:usagetip", "Select the components below that should be included in the exported scheme")position: Kirigami.InlineMessage.Position.Headertype: Kirigami.MessageType.InformationshowCloseButton: trueactions: [Kirigami.Action {icon.name: "document-export-symbolic"text: i18nc("@action:button Export shortcut scheme", "Export")Accessible.name: i18nc("@action:button accessible", "Export shortcut scheme")onTriggered: {shortcutSchemeFileDialogLoader.save = trueshortcutSchemeFileDialogLoader.active = trueexportActive = false}}]}Kirigami.SearchField {id: searchenabled: !errorOccured && !exportActiveLayout.fillWidth: true// Equal to the margins removed by disabling header paddingLayout.margins: Kirigami.Units.mediumSpacingBinding {target: kcm.filteredModelproperty: "filter"value: search.textrestoreMode: Binding.RestoreBinding}}}// Since we disabled the scroll views' frame and background, we're responsible// for setting the background color ourselves, because the background color// of the page it sits on top of doesn't have the right color for these views.Rectangle {anchors.fill: parentKirigami.Theme.inherit: falseKirigami.Theme.colorSet: Kirigami.Theme.Viewcolor: Kirigami.Theme.backgroundColorRowLayout {anchors.fill: parentenabled: !errorOccuredspacing: 0QQC2.ScrollView {id: categoryListLayout.preferredWidth: Kirigami.Units.gridUnit * 14Layout.fillHeight: trueclip: trueListView {id: componentsclip: truemodel: kcm.filteredModelactiveFocusOnTab: trueonActiveFocusChanged: currentIndex = Math.max(currentIndex, 0)Connections {target: kcm.shortcutsModelfunction onRowsInserted(parent, first, last) {const sourceIndex = kcm.shortcutsModel.index(last, 0)Qt.callLater(() => {const proxyIndex = kcm.filteredModel.mapFromSource(sourceIndex)if (proxyIndex.valid) {components.currentIndex = proxyIndex.row}})}}delegate: QQC2.ItemDelegate {id: componentDelegatereadonly property bool pendingDeletion: model.pendingDeletionfunction undoDeletion() {model.pendingDeletion = false;}width: ListView.view.widthtext: model.displayicon.name: model.decorationAccessible.onPressAction: clicked()KeyNavigation.right: shortcutsListonClicked: ListView.view.currentIndex = indexhighlighted: ListView.isCurrentItemcontentItem: RowLayout {spacing: Kirigami.Units.smallSpacingKirigami.IconTitleSubtitle {id: labelicon.name: componentDelegate.icon.nametitle: componentDelegate.textLayout.fillWidth: trueopacity: model.pendingDeletion ? 0.5 : 1.0selected: componentDelegate.highlighted || componentDelegate.down}QQC2.CheckBox {checked: model.checkedvisible: exportActiveAccessible.name: i18nc("@option:check accessible export shortcuts %1 is application/group name", "Export %1", model.display)onToggled: model.checked = checked}QQC2.Button {id: editButtonimplicitHeight: label.implicitHeightimplicitWidth: implicitHeighticon.name: "edit-rename"text: i18nc("@action:button %1 is the name of a custom command", "Edit command for %1", model.display)display: QQC2.AbstractButton.IconOnlyvisible: model.section === Private.ComponentType.Command&& !exportActive&& !model.pendingDeletion&& (componentDelegate.hovered || componentDelegate.ListView.isCurrentItem)onClicked: {addCommandDialog.editing = true;addCommandDialog.componentName = model.component;addCommandDialog.name = model.display;const componentIndex = kcm.filteredModel.index(index, 0);const actionIndex = kcm.filteredModel.index(0, 0, componentIndex);addCommandDialog.oldExec = kcm.filteredModel.data(actionIndex, Qt.DisplayRole);addCommandDialog.commandListItemDelegate = componentDelegate;addCommandDialog.open();}QQC2.ToolTip.text: textQQC2.ToolTip.visible: hovered || activeFocusQQC2.ToolTip.delay: Kirigami.Units.toolTipDelay}QQC2.Button {id: deleteButtonimplicitHeight: label.implicitHeightimplicitWidth: implicitHeighticon.name: "edit-delete"text: i18nc("@action:button %1 is the name of a shortcut category", "Remove all shortcuts for %1", model.display)display: QQC2.AbstractButton.IconOnlyvisible: (model?.section !== Private.ComponentType.CommonAction&& model?.isRemovable&& !exportActive&& !model?.pendingDeletion&& (componentDelegate.hovered || componentDelegate.ListView.isCurrentItem)) ?? falseonClicked: {model.pendingDeletion = true;componentDelegate.click();}QQC2.ToolTip.text: textQQC2.ToolTip.visible: hovered || activeFocusQQC2.ToolTip.delay: Kirigami.Units.toolTipDelay}QQC2.Button {implicitHeight: label.implicitHeightimplicitWidth: implicitHeighticon.name: "edit-undo"text: i18nc("@action:button", "Undo deletion")display: QQC2.AbstractButton.IconOnlyvisible: !exportActive && model.pendingDeletiononClicked: componentDelegate.undoDeletion()QQC2.ToolTip.text: textQQC2.ToolTip.visible: hovered || activeFocusQQC2.ToolTip.delay: Kirigami.Units.toolTipDelay}Rectangle {id: defaultIndicatorradius: width * 0.5implicitWidth: Kirigami.Units.largeSpacingimplicitHeight: Kirigami.Units.largeSpacingvisible: kcm.defaultsIndicatorsVisibleopacity: !model.isDefaultcolor: Kirigami.Theme.neutralTextColor}}}section.property: "section"section.delegate: Kirigami.ListSectionHeader {label: root.sectionNames[section]width: components.widthQQC2.CheckBox {id: sectionCheckboxLayout.alignment: Qt.AlignRight// width of indicator + layout spacingLayout.rightMargin: kcm.defaultsIndicatorsVisible ? Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0visible: exportActiveonToggled: {const checked = sectionCheckbox.checkedconst startIndex = kcm.shortcutsModel.index(0, 0)const indices = kcm.shortcutsModel.match(startIndex, Private.BaseModel.SectionRole, section, -1, Qt.MatchExactly)for (const index of indices) {kcm.shortcutsModel.setData(index, checked, Private.BaseModel.CheckedRole)}}Connections {enabled: exportActivetarget: kcm.shortcutsModelfunction onDataChanged (topLeft, bottomRight, roles) {const startIndex = kcm.shortcutsModel.index(0, 0)const indices = kcm.shortcutsModel.match(startIndex, Private.BaseModel.SectionRole, section, -1, Qt.MatchExactly)sectionCheckbox.checked = indices.reduce((acc, index) => acc && kcm.shortcutsModel.data(index, Private.BaseModel.CheckedRole), true)}}}}onCurrentItemChanged: {if (!currentItem) {currentIndex = -1;return;}dm.rootIndex = kcm.filteredModel.index(currentIndex, 0)}onCurrentIndexChanged: {shortcutsList.selectedIndex = -1;}Kirigami.PlaceholderMessage {anchors.centerIn: parentwidth: parent.width - (Kirigami.Units.largeSpacing * 4)visible: components.count === 0 && search.text.length > 0text: i18nc("@info:status placeholdermessage", "No items matched the search terms")}}}Kirigami.Separator {Layout.fillHeight: true}ColumnLayout {spacing: 0Kirigami.InlineMessage {Layout.fillWidth: truevisible: shortcutsList.contentsWillBeDeletedposition: Kirigami.InlineMessage.Position.Headertype: Kirigami.MessageType.Warningtext: i18nc("@info", "These shortcuts will be deleted when changes are applied.")actions: [Kirigami.Action {icon.name: "edit-undo-symbolic"text: i18nc("@action:button", "Undo")Accessible.name: i18nc("@action:button", "Undo deleting shortcuts")onTriggered: components.currentItem.undoDeletion();}]}QQC2.ScrollView {Layout.fillHeight: trueLayout.fillWidth: trueenabled: !root.exportActiveclip: trueListView {id: shortcutsListreadonly property bool contentsWillBeDeleted: !root.exportActive && (components.currentItem ? components.currentItem.pendingDeletion : false)property int selectedIndex: -1clip: trueactiveFocusOnTab: truemodel: DelegateModel {id: dmonRootIndexChanged: shortcutsList.positionViewAtBeginning()model: rootIndex.valid ? kcm.filteredModel : undefineddelegate: ShortcutActionDelegate {showExpandButton: shortcutsList.count > 1enabled: !shortcutsList.contentsWillBeDeletedonHeightChanged: {if (shortcutsList.selectedIndex == index) {ListView.view.positionViewAtIndex(index, ListView.Contain)}}}KeyNavigation.left: components}Kirigami.PlaceholderMessage {anchors.centerIn: parentwidth: parent.width - (Kirigami.Units.largeSpacing * 4)visible: components.currentIndex == -1text: i18nc("@info:usagetip placeholdermessage","Select an item from the list to view its shortcuts here")}}}}}}Loader {id: shortcutSchemeFileDialogLoaderactive: falseproperty bool savesourceComponent: FileDialog {title: save ? i18nc("@title:window dialog", "Export Shortcut Scheme") : i18nc("@title:window dialog", "Import Shortcut Scheme")currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]nameFilters: [ i18nc("Template for file dialog","Shortcut Scheme (*.kksrc)") ]defaultSuffix: ".kksrc"fileMode: shortcutSchemeFileDialogLoader.save ? FileDialog.SaveFile : FileDialog.OpenFileComponent.onCompleted: open()onAccepted: {if (save) {kcm.writeScheme(selectedFile)} else {var schemes = schemeBox.modelschemes.splice(schemes.length - 1, 0, {name: kcm.urlFilename(selectedFile), url: selectedFile})schemeBox.model = schemesschemeBox.currentIndex = schemes.length - 2}shortcutSchemeFileDialogLoader.active = false}onRejected: shortcutSchemeFileDialogLoader.active = false}}Kirigami.PromptDialog {id: addCommandDialogproperty bool editing: falseproperty string componentName: ""property string oldExec: ""property string name: ""property Item commandListItemDelegate: nulltitle: editing ? i18nc("@title:window dialog", "Edit Command") : i18nc("@title:window dialog", "Add Command")iconName: 'folder-script-symbolic'onVisibleChanged: {if (visible) {cmdField.clear();nameField.clear();cmdField.forceActiveFocus();if (editing) {cmdField.text = oldExec;nameField.text = name;cmdField.selectAll();}}}property Kirigami.Action addCommandAction: Kirigami.Action {text: addCommandDialog.editing ? i18nc("@action:button in dialog, save changes to custom command", "Save") : i18nc("@action:button in dialog, add entry as new custom command", "Add")Accessible.name: addCommandDialog.editing? i18nc("@action:button accessible", "Save command"): i18nc("@action:button accessible", "Add command")icon.name: addCommandDialog.editing ? "dialog-ok" : "list-add"enabled: cmdField.length > 0onTriggered: {if (addCommandDialog.editing) {kcm.editCommand(addCommandDialog.componentName, nameField.text, cmdField.text);} else {kcm.addCommand(cmdField.text, nameField.text);}addCommandDialog.close();}}standardButtons: Kirigami.Dialog.CancelcustomFooterActions: [addCommandAction]ColumnLayout {spacing: Kirigami.Units.smallSpacingQQC2.Label {Layout.fillWidth: truetext: i18nc("@info:usagetip", "Enter a command or choose a script file:")textFormat: Text.PlainTextwrapMode: Text.Wrap}Kirigami.FormLayout {RowLayout {Layout.fillWidth: trueKirigami.FormData.label: i18nc("@label:textbox A shell command", "Command:")spacing: Kirigami.Units.smallSpacingQQC2.TextField {id: cmdFieldLayout.fillWidth: trueAccessible.name: i18nc("@label:textbox accessible", "Command")font.family: "monospace"onAccepted: addCommandDialog.addCommandAction.triggered()}QQC2.Button {icon.name: "document-open"text: i18nc("@action:button", "Chooseβ¦")Accessible.name: i18nc("@action:button accessible", "Choose script file")onClicked: {openScriptFileDialogLoader.active = true}}}QQC2.TextField {id: nameFieldKirigami.FormData.label: i18nc("@label:textfield Human-readable name given to a shell command", "Name:")placeholderText: i18nc("@info:placeholder", "Enter name here")onAccepted: addCommandDialog.addCommandAction.triggered()}}}}Loader {id: openScriptFileDialogLoaderactive: falsesourceComponent: FileDialog {title: i18nc("@title:window", "Choose Script File")currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]nameFilters: [ i18nc("Filename filters for file dialog. Do not translate any of the filename extensions","Script files (.sh, .bash, .zsh, etc.) (*.*sh)") ]Component.onCompleted: open()onAccepted: {cmdField.text = selectedFilecmdField.text = kcm.quoteUrl(selectedFile)nameField.text = kcm.findBaseName(cmdField.text)openScriptFileDialogLoader.active = false}onRejected: openScriptFileDialogLoader.active = false}}Kirigami.Dialog {id: importSheettitle: i18nc("@title:window dialog", "Import Shortcut Scheme")width: Math.max(Math.round(root.width / 2), Kirigami.Units.gridUnit * 24)ColumnLayout {spacing: Kirigami.Units.smallSpacingQQC2.Label {text: i18nc("@label:listbox regarding 'shortcut schemes'", "Select the scheme to import:")textFormat: Text.PlainTextLayout.leftMargin: Kirigami.Units.largeSpacingLayout.rightMargin: Kirigami.Units.largeSpacing}QQC2.ComboBox {id: schemeBoxLayout.leftMargin: Kirigami.Units.largeSpacingLayout.rightMargin: Kirigami.Units.largeSpacingreadonly property bool customSchemeSelected: currentIndex == count - 1property string url: ""currentIndex: count - 1textRole: "name"onActivated: url = model[index]["url"]Component.onCompleted: {var defaultSchemes = kcm.defaultSchemes()defaultSchemes.push({name: i18nc("@item:inlistbox regarding 'shortcut schemes'", "Custom Scheme"), url: "unused"})model = defaultSchemes}}}customFooterActions: [Kirigami.Action {text: schemeBox.customSchemeSelected ? i18nc("@action:button", "Select Fileβ¦") : i18nc("@action:button import shortcut scheme", "Import")Accessible.name: schemeBox.customSchemeSelected? i18nc("@action:button accessible", "Select shortcut scheme file"): i18nc("@action:button accessible", "Add shortcut scheme")Accessible.role: Accessible.MenuItemonTriggered: {if (schemeBox.customSchemeSelected) {shortcutSchemeFileDialogLoader.save = false;shortcutSchemeFileDialogLoader.active = true;} else {kcm.loadScheme(schemeBox.model[schemeBox.currentIndex]["url"])importSheet.close()}}}]}}