[utilities/keepsecret] src: WIP: Add multi-select with Ctrl+click and Shift+click

Roshani Kumari <[email protected]> Tue, 4 Aug 2026 13:48:02 +0000 (UTC)
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit f3d7322f61a08c4576b440f5b3a4d54b6bc9d50c by Roshani Kumari.
Committed on 04/08/2026 at 13:39.
Pushed by roshani into branch 'master'.

WIP: Add multi-select with Ctrl+click and Shift+click

M  +8    -0    src/collectionmodel.cpp
M  +1    -0    src/collectionmodel.h
M  +93   -17   src/qml/CollectionContentsPage.qml

https://invent.kde.org/utilities/keepsecret/-/commit/f3d7322f61a08c4576b440f5b3a4d54b6bc9d50c

diff --git a/src/collectionmodel.cpp b/src/collectionmodel.cpp
index 9da47ba..02b5dff 100644
--- a/src/collectionmodel.cpp
+++ b/src/collectionmodel.cpp
@@ -306,4 +306,12 @@ QVariantList CollectionModel::exportItems()
     return result;
 }
 
+QString CollectionModel::dbusPathAt(int row) const
+{
+    if (row < 0 || row >= m_items.count()) {
+        return QString();
+    }
+    return m_items.at(row).dbusPath;
+}
+
 #include "moc_collectionmodel.cpp"
diff --git a/src/collectionmodel.h b/src/collectionmodel.h
index fdf1f05..3b5c0fb 100644
--- a/src/collectionmodel.h
+++ b/src/collectionmodel.h
@@ -37,6 +37,7 @@ public:
 
     Q_INVOKABLE void lock();
     Q_INVOKABLE void unlock();
+    Q_INVOKABLE QString dbusPathAt(int row) const;
 
     QHash<int, QByteArray> roleNames() const override;
     int rowCount(const QModelIndex &parent = QModelIndex()) const override;
diff --git a/src/qml/CollectionContentsPage.qml b/src/qml/CollectionContentsPage.qml
index 3e5a059..ff9f785 100644
--- a/src/qml/CollectionContentsPage.qml
+++ b/src/qml/CollectionContentsPage.qml
@@ -15,6 +15,9 @@ Kirigami.ScrollablePage {
     id: page
 
     property alias currentEntry: view.currentIndex
+    property var selectedIndices: []
+    property int lastSelectedIndex: -1
+    property int selectedCount: 0
 
     title: App.collectionModel.collectionName
 
@@ -94,6 +97,36 @@ Kirigami.ScrollablePage {
             AC.ActionCollection.action: "export-wallet"
             onTriggered: exportDialog.open()
         },
+        Kirigami.Action {
+            id: deleteSelectedAction
+            text: i18nc("@action:button Delete selected secrets", "Delete Selected")
+            icon.name: "delete-symbolic"
+            displayHint: Kirigami.DisplayHint.AlwaysHide
+            visible: true
+            enabled: page.selectedCount > 1
+            onTriggered: {
+                console.log("Delete Selected triggered, count:", page.selectedIndices.length)
+                showDeleteDialog(
+                    i18nc("@title:window", "Delete Secrets"),
+                    i18nc("@label", "Are you sure you want to delete %1 items?", page.selectedIndices.length),
+                    i18nc("@action:check", "I understand that the items will be permanently deleted"),
+                    () => {
+                        const indices = [...page.selectedIndices]
+                        // First collect all dbus paths
+                        console.log("Selected indices:", JSON.stringify(indices))
+                        const paths = indices.map(idx => App.collectionModel.dbusPathAt(idx)).filter(p => p)
+                        // Then delete all
+                        console.log("Paths to delete:", JSON.stringify(paths))
+                        paths.forEach(dbusPath => {
+                            console.log("Deleting:", dbusPath)
+                            App.secretItemForContextMenu.loadItem(App.collectionModel.collectionPath, dbusPath)
+                            App.secretItemForContextMenu.deleteItem()
+                        })
+                        page.selectedIndices = []
+                    }
+                );
+            }
+        },
         Kirigami.Action {
             text: i18nc("@action:inmenu", "Import")
             icon.name: "document-import"
@@ -306,27 +339,57 @@ Kirigami.ScrollablePage {
             required property var model
             required property int index
             width: view.width
-            // FIXME: this imitates an item with the space for the icon even if there is none, there should be something to do that more cleanly
             leftPadding: Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.largeSpacing * 2
             text: model.display
-            highlighted: view.currentIndex == index
- 
-            function click() {
-                if (contextMenu.visible) {
-                    return;
+            highlighted: view.currentIndex == index || page.selectedIndices.indexOf(index) !== -1
+
+            TapHandler {
+                acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+                acceptedModifiers: Qt.NoModifier
+                onTapped: {
+                    if (contextMenu.visible) return
+                    page.selectedIndices = []
+                    page.selectedCount = 0
+                    page.lastSelectedIndex = index
+                    view.currentIndex = index
+                    App.secretItem.loadItem(App.collectionModel.collectionPath, model.dbusPath)
+                    view.forceActiveFocus()
                 }
-                view.currentIndex = index
-                App.secretItem.loadItem(App.collectionModel.collectionPath, model.dbusPath);
-                view.forceActiveFocus();
             }
 
-            onClicked: click()
-            Keys.onPressed: (event) => {
-                if (contextMenu.visible) {
-                    return;
+            TapHandler {
+                acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+                acceptedModifiers: Qt.ControlModifier
+                onTapped: {
+                if (contextMenu.visible) return
+                let newSelection = [...page.selectedIndices]
+                const idx = newSelection.indexOf(index)
+                if (idx === -1) {
+                    newSelection.push(index)
+                } else {
+                    newSelection.splice(idx, 1)
                 }
-                if (event.key == Qt.Key_Enter || event.key == Qt.Key_Return) {
-                    delegate.click();
+                page.selectedIndices = newSelection
+                page.selectedCount = page.selectedIndices.length
+                page.lastSelectedIndex = index
+                console.log("selectedCount:", page.selectedCount)
+                }
+            }
+
+            TapHandler {
+                acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+                acceptedModifiers: Qt.ShiftModifier
+                onTapped: {
+                    if (contextMenu.visible) return
+                    if (page.lastSelectedIndex === -1) return
+                    const start = Math.min(page.lastSelectedIndex, index)
+                    const end = Math.max(page.lastSelectedIndex, index)
+                    let newSelection = []
+                    for (let i = start; i <= end; i++) {
+                        newSelection.push(i)
+                    }
+                    page.selectedIndices = newSelection
+                    page.selectedCount = page.selectedIndices.length
                 }
             }
 
@@ -335,11 +398,13 @@ Kirigami.ScrollablePage {
                 acceptedButtons: Qt.RightButton
                 onPressedChanged: {
                     if (pressed) {
-                        contextMenu.model = model
-                        contextMenu.popup(delegate)
+                    contextMenu.model = model
+                    contextMenu.popup(delegate)
                     }
                 }
             }
+
+            // Long press (touch)
             TapHandler {
                 acceptedDevices: PointerDevice.TouchScreen
                 onLongPressed: {
@@ -347,6 +412,17 @@ Kirigami.ScrollablePage {
                     contextMenu.popup(delegate)
                 }
             }
+
+            Keys.onPressed: (event) => {
+                if (contextMenu.visible) return
+                if (event.key == Qt.Key_Enter || event.key == Qt.Key_Return) {
+                    page.selectedIndices = []
+                    page.selectedCount = 0
+                    view.currentIndex = index
+                    App.secretItem.loadItem(App.collectionModel.collectionPath, model.dbusPath)
+                    view.forceActiveFocus()
+                }
+            }
         }
 
         Kirigami.PlaceholderMessage {