🏑 index : quine-global-labs/mirror-kde-plasma-desktop.git

/*
    SPDX-FileCopyrightText: 2020 David Redondo <david@david-redondo.de>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

import QtCore
import QtQuick
import QtQuick.Dialogs
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import QtQml
import QtQml.Models

import org.kde.kirigami as Kirigami
import org.kde.kcmutils as KCM
import org.kde.private.kcms.keys as Private

KCM.AbstractKCM {
    id: root
    implicitWidth: Kirigami.Units.gridUnit * 44
    implicitHeight: Kirigami.Units.gridUnit * 33

    framedView: false

    // order must be in sync with ComponentType enum in basemodel.h
    readonly 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.visible
    readonly property bool errorOccured: kcm.lastError !== ""

    Connections {
        target: kcm
        function onShowComponent(row) {
            components.currentIndex = row
        }
    }

    actions: [
        Kirigami.Action {
            enabled: !exportActive
            icon.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.KeepVisible

            enabled: !exportActive

            Kirigami.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.MenuItem
                onTriggered: 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.MenuItem
                onTriggered: {
                    addCommandDialog.editing = false
                    addCommandDialog.open()
                }
            }
        }
    ]

    headerPaddingEnabled: false // Let the InlineMessage touch the edges
    header: ColumnLayout {
        spacing: 0

        Kirigami.InlineMessage {
            Layout.fillWidth: true
            visible: errorOccured
            text: kcm.lastError
            position: Kirigami.InlineMessage.Position.Header
            type: Kirigami.MessageType.Error
        }
        Kirigami.InlineMessage {
            id: exportWarning
            Layout.fillWidth: true
            text: i18nc("@info:status inlinemessage", "Cannot export scheme while there are unsaved changes")
            position: Kirigami.InlineMessage.Position.Header
            type: Kirigami.MessageType.Warning
            showCloseButton: true
            Binding on visible {
                when: exportWarning.visible
                value: kcm.needsSave
                restoreMode: Binding.RestoreNone
            }
        }
        Kirigami.InlineMessage {
            id: exportInfo
            Layout.fillWidth: true
            text: i18nc("@info:usagetip", "Select the components below that should be included in the exported scheme")
            position: Kirigami.InlineMessage.Position.Header
            type: Kirigami.MessageType.Information
            showCloseButton: true
            actions: [
                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 = true
                        shortcutSchemeFileDialogLoader.active = true
                        exportActive = false
                    }
                }
            ]
        }
        Kirigami.SearchField  {
            id: search
            enabled: !errorOccured && !exportActive
            Layout.fillWidth: true
            // Equal to the margins removed by disabling header padding
            Layout.margins: Kirigami.Units.mediumSpacing
            Binding {
                target: kcm.filteredModel
                property: "filter"
                value: search.text
                restoreMode: 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: parent
        Kirigami.Theme.inherit: false
        Kirigami.Theme.colorSet: Kirigami.Theme.View
        color: Kirigami.Theme.backgroundColor

        RowLayout  {
            anchors.fill: parent
            enabled: !errorOccured
            spacing: 0

            QQC2.ScrollView {
                id: categoryList

                Layout.preferredWidth: Kirigami.Units.gridUnit * 14
                Layout.fillHeight: true
                clip: true

                ListView {
                    id: components
                    clip: true
                    model: kcm.filteredModel
                    activeFocusOnTab: true
                    onActiveFocusChanged: currentIndex = Math.max(currentIndex, 0)

                    Connections {
                        target: kcm.shortcutsModel
                        function 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: componentDelegate

                        readonly property bool pendingDeletion: model.pendingDeletion

                        function undoDeletion() {
                            model.pendingDeletion = false;
                        }

                        width: ListView.view.width

                        text: model.display
                        icon.name: model.decoration

                        Accessible.onPressAction: clicked()
                        KeyNavigation.right: shortcutsList

                        onClicked: ListView.view.currentIndex = index
                        highlighted: ListView.isCurrentItem

                        contentItem: RowLayout {
                            spacing: Kirigami.Units.smallSpacing
                            Kirigami.IconTitleSubtitle {
                                id: label

                                icon.name: componentDelegate.icon.name
                                title: componentDelegate.text
                                Layout.fillWidth: true
                                opacity: model.pendingDeletion ? 0.5 : 1.0
                                selected: componentDelegate.highlighted || componentDelegate.down
                            }

                            QQC2.CheckBox {
                                checked: model.checked
                                visible: exportActive
                                Accessible.name: i18nc("@option:check accessible export shortcuts %1 is application/group name", "Export %1", model.display)
                                onToggled: model.checked = checked
                            }
                            QQC2.Button {
                                id: editButton

                                implicitHeight: label.implicitHeight
                                implicitWidth: implicitHeight

                                icon.name: "edit-rename"
                                text: i18nc("@action:button %1 is the name of a custom command", "Edit command for %1", model.display)
                                display: QQC2.AbstractButton.IconOnly

                                visible: 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: text
                                QQC2.ToolTip.visible: hovered || activeFocus
                                QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
                            }
                            QQC2.Button {
                                id: deleteButton

                                implicitHeight: label.implicitHeight
                                implicitWidth: implicitHeight

                                icon.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.IconOnly

                                visible: (model?.section !== Private.ComponentType.CommonAction
                                         && model?.isRemovable
                                         && !exportActive
                                         && !model?.pendingDeletion
                                         && (componentDelegate.hovered || componentDelegate.ListView.isCurrentItem)) ?? false
                                onClicked: {
                                    model.pendingDeletion = true;
                                    componentDelegate.click();
                                }

                                QQC2.ToolTip.text: text
                                QQC2.ToolTip.visible: hovered || activeFocus
                                QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
                            }
                            QQC2.Button {
                                implicitHeight: label.implicitHeight
                                implicitWidth: implicitHeight

                                icon.name: "edit-undo"
                                text: i18nc("@action:button", "Undo deletion")
                                display: QQC2.AbstractButton.IconOnly

                                visible: !exportActive && model.pendingDeletion
                                onClicked: componentDelegate.undoDeletion()

                                QQC2.ToolTip.text: text
                                QQC2.ToolTip.visible: hovered || activeFocus
                                QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
                            }
                            Rectangle {
                                id: defaultIndicator
                                radius: width * 0.5
                                implicitWidth: Kirigami.Units.largeSpacing
                                implicitHeight: Kirigami.Units.largeSpacing
                                visible: kcm.defaultsIndicatorsVisible
                                opacity: !model.isDefault
                                color: Kirigami.Theme.neutralTextColor
                            }
                        }
                    }

                    section.property: "section"
                    section.delegate: Kirigami.ListSectionHeader {
                        label: root.sectionNames[section]
                        width:  components.width
                        QQC2.CheckBox {
                            id: sectionCheckbox
                            Layout.alignment: Qt.AlignRight
                            // width of indicator + layout spacing
                            Layout.rightMargin: kcm.defaultsIndicatorsVisible ? Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing : 0
                            visible: exportActive
                            onToggled: {
                                const checked = sectionCheckbox.checked
                                const 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: exportActive
                                target: kcm.shortcutsModel
                                function 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: parent
                        width: parent.width - (Kirigami.Units.largeSpacing * 4)
                        visible: components.count === 0 && search.text.length > 0
                        text: i18nc("@info:status placeholdermessage", "No items matched the search terms")
                    }
                }
            }

            Kirigami.Separator {
                Layout.fillHeight: true
            }

            ColumnLayout {
                spacing: 0

                Kirigami.InlineMessage {
                    Layout.fillWidth: true
                    visible: shortcutsList.contentsWillBeDeleted
                    position: Kirigami.InlineMessage.Position.Header
                    type: Kirigami.MessageType.Warning
                    text: 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: true
                    Layout.fillWidth: true

                    enabled: !root.exportActive
                    clip: true

                    ListView {
                        id: shortcutsList

                        readonly property bool contentsWillBeDeleted: !root.exportActive && (components.currentItem ? components.currentItem.pendingDeletion : false)
                        property int selectedIndex: -1

                        clip: true
                        activeFocusOnTab: true

                        model: DelegateModel {
                            id: dm

                            onRootIndexChanged: shortcutsList.positionViewAtBeginning()
                            model: rootIndex.valid ?  kcm.filteredModel : undefined
                            delegate: ShortcutActionDelegate {
                                showExpandButton: shortcutsList.count > 1
                                enabled: !shortcutsList.contentsWillBeDeleted
                                onHeightChanged: {
                                    if (shortcutsList.selectedIndex == index) {
                                        ListView.view.positionViewAtIndex(index, ListView.Contain)
                                    }
                                }
                            }
                            KeyNavigation.left: components
                        }

                        Kirigami.PlaceholderMessage {
                            anchors.centerIn: parent
                            width: parent.width - (Kirigami.Units.largeSpacing * 4)
                            visible: components.currentIndex == -1
                            text: i18nc("@info:usagetip placeholdermessage","Select an item from the list to view its shortcuts here")
                        }
                    }
                }
            }
        }
    }

    Loader {
        id: shortcutSchemeFileDialogLoader
        active: false
        property bool save
        sourceComponent: 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.OpenFile
            Component.onCompleted: open()
            onAccepted: {
                if (save) {
                    kcm.writeScheme(selectedFile)
                } else {
                    var schemes = schemeBox.model
                    schemes.splice(schemes.length - 1, 0, {name: kcm.urlFilename(selectedFile), url: selectedFile})
                    schemeBox.model = schemes
                    schemeBox.currentIndex = schemes.length - 2
                }
                shortcutSchemeFileDialogLoader.active = false
            }
            onRejected: shortcutSchemeFileDialogLoader.active = false
        }
    }

    Kirigami.PromptDialog {
        id: addCommandDialog
        property bool editing: false
        property string componentName: ""
        property string oldExec: ""
        property string name: ""
        property Item commandListItemDelegate: null

        title: 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 > 0
            onTriggered: {
                if (addCommandDialog.editing) {
                    kcm.editCommand(addCommandDialog.componentName, nameField.text, cmdField.text);
                } else {
                    kcm.addCommand(cmdField.text, nameField.text);
                }
                addCommandDialog.close();
            }
        }

        standardButtons: Kirigami.Dialog.Cancel

        customFooterActions: [addCommandAction]

        ColumnLayout {
            spacing: Kirigami.Units.smallSpacing

            QQC2.Label {
                Layout.fillWidth: true
                text: i18nc("@info:usagetip", "Enter a command or choose a script file:")
                textFormat: Text.PlainText
                wrapMode: Text.Wrap
            }

            Kirigami.FormLayout {

                RowLayout {
                    Layout.fillWidth: true
                    Kirigami.FormData.label: i18nc("@label:textbox A shell command", "Command:")
                    spacing: Kirigami.Units.smallSpacing

                    QQC2.TextField {
                        id: cmdField
                        Layout.fillWidth: true
                        Accessible.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: nameField
                    Kirigami.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: openScriptFileDialogLoader
        active: false
        sourceComponent: 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 = selectedFile
                cmdField.text = kcm.quoteUrl(selectedFile)
                nameField.text = kcm.findBaseName(cmdField.text)
                openScriptFileDialogLoader.active = false
            }
            onRejected: openScriptFileDialogLoader.active = false
        }
    }

    Kirigami.Dialog {
        id: importSheet

        title: i18nc("@title:window dialog", "Import Shortcut Scheme")

        width: Math.max(Math.round(root.width / 2), Kirigami.Units.gridUnit * 24)

        ColumnLayout {
            spacing: Kirigami.Units.smallSpacing

            QQC2.Label {
                text: i18nc("@label:listbox regarding 'shortcut schemes'", "Select the scheme to import:")
                textFormat: Text.PlainText
                Layout.leftMargin: Kirigami.Units.largeSpacing
                Layout.rightMargin: Kirigami.Units.largeSpacing
            }

            QQC2.ComboBox {
                id: schemeBox

                Layout.leftMargin: Kirigami.Units.largeSpacing
                Layout.rightMargin: Kirigami.Units.largeSpacing

                readonly property bool customSchemeSelected: currentIndex == count - 1
                property string url: ""
                currentIndex: count - 1
                textRole: "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.MenuItem

                onTriggered: {
                    if (schemeBox.customSchemeSelected) {
                        shortcutSchemeFileDialogLoader.save = false;
                        shortcutSchemeFileDialogLoader.active = true;
                    } else {
                        kcm.loadScheme(schemeBox.model[schemeBox.currentIndex]["url"])
                        importSheet.close()
                    }
                }
            }
        ]
    }
}