Add trash button to convergence dock

Show a trash icon to the left of the Overview button in convergence
mode. The icon toggles between user-trash and user-trash-full by
watching ~/.local/share/Trash/files via FolderListModel.

Left-click opens the trash in the default file manager via
Qt.openUrlExternally("trash:/"), making the feature file-manager-
agnostic. Right-click offers "Open Trash" and "Empty Trash"; the
latter shows a confirmation dialog before invoking KIO.Trash via
D-Bus.

The right wing of the virtual-desktop pager shifts left to make
room for the trash button.
This commit is contained in:
Marco Allegretti 2026-05-01 12:48:49 +02:00
parent a0bad0507f
commit 6d79f8ed05
3 changed files with 130 additions and 2 deletions

View file

@ -108,4 +108,13 @@ void HomeScreen::activateVirtualDesktop(const QVariant &desktop) const
virtualDesktopInfo.requestActivate(desktop);
}
void HomeScreen::emptyTrash() const
{
QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kio.trash"),
QStringLiteral("/trash"),
QStringLiteral("org.kde.KIO.Trash"),
QStringLiteral("emptyTrash"));
QDBusConnection::sessionBus().send(message);
}
#include "homescreen.moc"

View file

@ -51,6 +51,7 @@ public:
Q_INVOKABLE void triggerOverview() const;
Q_INVOKABLE void activateVirtualDesktop(const QVariant &desktop) const;
Q_INVOKABLE void emptyTrash() const;
FolioSettings *folioSettings();
HomeScreenState *homeScreenState();

View file

@ -4,6 +4,8 @@
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts 1.1
import QtCore
import Qt.labs.folderlistmodel
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell.state as MobileShellState
@ -91,6 +93,7 @@ MouseArea {
readonly property real pagerButtonWidth: showPager ? Math.min(root.height, Kirigami.Units.gridUnit * 2.5) : 0
readonly property int pagerLeftCount: showPager ? Math.ceil(virtualDesktopInfo.numberOfDesktops / 2) : 0
readonly property int pagerRightCount: showPager ? virtualDesktopInfo.numberOfDesktops - pagerLeftCount : 0
readonly property real trashButtonWidth: convergenceMode ? root.height : 0
function pagerDesktopName(index) {
let names = virtualDesktopInfo.desktopNames
@ -109,7 +112,7 @@ MouseArea {
return (ids && i < ids.length) ? String(ids[i]) : ""
}
for (let i = 0; i < pagerRightCount; ++i) {
let bx = root.width - navButtonWidth - (pagerRightCount - i) * pagerButtonWidth
let bx = root.width - navButtonWidth - root.trashButtonWidth - (pagerRightCount - i) * pagerButtonWidth
if (x >= bx && x < bx + pagerButtonWidth) {
let di = pagerLeftCount + i
return (ids && di < ids.length) ? String(ids[di]) : ""
@ -371,7 +374,7 @@ MouseArea {
return root.pagerButtonDesktopAt(cx) === desktopId
}
x: root.width - root.navButtonWidth - (root.pagerRightCount - index) * root.pagerButtonWidth
x: root.width - root.navButtonWidth - root.trashButtonWidth - (root.pagerRightCount - index) * root.pagerButtonWidth
y: 0
width: root.pagerButtonWidth
height: root.height
@ -413,6 +416,121 @@ MouseArea {
}
}
// ---- Trash button (convergence mode, sits between the right pager wing and the Overview button) ----
// Watches ~/.local/share/Trash/files to detect empty/full state.
// FolderListModel reacts to directory changes automatically.
FolderListModel {
id: trashFilesModel
folder: StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Trash/files"
showFiles: true
showDirs: true
showDotAndDotDot: false
}
// Confirmation dialog for "Empty Trash" parented to the homescreen so it
// is sized correctly and floats above all dock content.
Loader {
id: emptyTrashDialogLoader
parent: root.homeScreen
anchors.fill: parent
active: false
function open() {
active = true;
item.open();
}
sourceComponent: Kirigami.PromptDialog {
title: i18n("Empty Trash")
subtitle: i18n("Permanently delete all items in the trash? This action cannot be undone.")
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.Cancel
onAccepted: root.folio.emptyTrash()
onClosed: emptyTrashDialogLoader.active = false
}
}
Rectangle {
id: trashButton
visible: root.convergenceMode
activeFocusOnTab: root.convergenceMode
x: root.width - root.navButtonWidth - root.trashButtonWidth
y: 0
width: root.trashButtonWidth
height: root.height
color: "transparent"
Accessible.role: Accessible.Button
Accessible.name: i18n("Trash")
Accessible.onPressAction: Qt.openUrlExternally("trash:/")
Keys.onReturnPressed: Qt.openUrlExternally("trash:/")
Keys.onEnterPressed: Qt.openUrlExternally("trash:/")
Keys.onSpacePressed: Qt.openUrlExternally("trash:/")
KeyboardHighlight {
anchors.fill: parent
visible: trashButton.activeFocus
}
Rectangle {
anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius
color: root.dockItemColor(trashMouseArea.containsPress, trashMouseArea.containsMouse, false)
Behavior on color {
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
}
}
Kirigami.Icon {
anchors.centerIn: parent
width: root.dockIconSize
height: width
source: trashFilesModel.count > 0 ? "user-trash-full" : "user-trash"
active: trashMouseArea.containsMouse
}
PC3.ToolTip {
visible: trashMouseArea.containsMouse
text: trashFilesModel.count > 0
? i18np("Trash — 1 item", "Trash — %1 items", trashFilesModel.count)
: i18n("Trash")
}
MouseArea {
id: trashMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
trashContextMenu.open()
} else {
Qt.openUrlExternally("trash:/")
}
}
}
PC3.Menu {
id: trashContextMenu
popupType: T.Popup.Window
PC3.MenuItem {
icon.name: "folder-open"
text: i18n("Open Trash")
onTriggered: Qt.openUrlExternally("trash:/")
}
PC3.MenuItem {
icon.name: "trash-empty"
text: i18n("Empty Trash")
enabled: trashFilesModel.count > 0
onTriggered: emptyTrashDialogLoader.open()
}
}
}
TaskManager.VirtualDesktopInfo {
id: virtualDesktopInfo
}