mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-30 01:22:06 -04:00
dock: add trash bin button (#2277)
* dock: add trash bin button - icon reflects content- filled/empty - multiple file manager support with nautilus as default, builtin as fallback - settingsspec at dock tab - context menu * fix: remove support for builtin filebrowser needs specific adaptors at FB adhering the trash freedesktop spec * fix: suppress auto-hide dock with trash context menu open * feat: allow for custom file manager command * feat: switch runner to proc.runcommand with toasts on command failures
This commit is contained in:
@@ -545,6 +545,9 @@ Singleton {
|
||||
property int dockMaxVisibleApps: 0
|
||||
property int dockMaxVisibleRunningApps: 0
|
||||
property bool dockShowOverflowBadge: true
|
||||
property bool dockShowTrash: false
|
||||
property string dockTrashFileManager: "nautilus"
|
||||
property string dockTrashCustomCommand: ""
|
||||
|
||||
property bool notificationOverlayEnabled: false
|
||||
property bool notificationPopupShadowEnabled: true
|
||||
|
||||
@@ -350,6 +350,9 @@ var SPEC = {
|
||||
dockMaxVisibleApps: { def: 0 },
|
||||
dockMaxVisibleRunningApps: { def: 0 },
|
||||
dockShowOverflowBadge: { def: true },
|
||||
dockShowTrash: { def: false },
|
||||
dockTrashFileManager: { def: "nautilus" },
|
||||
dockTrashCustomCommand: { def: "" },
|
||||
|
||||
notificationOverlayEnabled: { def: false },
|
||||
notificationPopupShadowEnabled: { def: true },
|
||||
|
||||
@@ -4,6 +4,7 @@ import qs.Common
|
||||
import qs.Modals
|
||||
import qs.Modals.Changelog
|
||||
import qs.Modals.Clipboard
|
||||
import qs.Modals.Common
|
||||
import qs.Modals.Greeter
|
||||
import qs.Modals.Settings
|
||||
import qs.Modals.DankLauncherV2
|
||||
@@ -284,11 +285,15 @@ Item {
|
||||
|
||||
sourceComponent: Dock {
|
||||
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
|
||||
trashContextMenu: dockTrashContextMenuLoader.item ? dockTrashContextMenuLoader.item : null
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
dockContextMenuLoader.active = true;
|
||||
if (SettingsData.dockShowTrash) {
|
||||
dockTrashContextMenuLoader.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +345,43 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: dockTrashContextMenuLoader
|
||||
|
||||
active: false
|
||||
|
||||
DockTrashContextMenu {
|
||||
id: dockTrashContextMenu
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onDockShowTrashChanged() {
|
||||
if (SettingsData.dockShowTrash) {
|
||||
dockTrashContextMenuLoader.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmModal {
|
||||
id: emptyTrashConfirm
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: TrashService
|
||||
function onEmptyTrashConfirmRequested(itemCount) {
|
||||
emptyTrashConfirm.showWithOptions({
|
||||
title: I18n.tr("Empty Trash?"),
|
||||
message: I18n.tr("Permanently delete %1 item(s)? This cannot be undone.").arg(itemCount),
|
||||
confirmText: I18n.tr("Empty"),
|
||||
cancelText: I18n.tr("Cancel"),
|
||||
confirmColor: Theme.error,
|
||||
onConfirm: () => TrashService.emptyTrash()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
id: notificationCenterLoader
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ Variants {
|
||||
model: SettingsData.getFilteredScreens("dock")
|
||||
|
||||
property var contextMenu
|
||||
property var trashContextMenu
|
||||
|
||||
delegate: PanelWindow {
|
||||
id: dock
|
||||
@@ -120,7 +121,7 @@ Variants {
|
||||
return Math.round(v * _dpr) / _dpr;
|
||||
}
|
||||
|
||||
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
|
||||
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData) || (dockVariants.trashContextMenu && dockVariants.trashContextMenu.visible && dockVariants.trashContextMenu.screen === modelData)
|
||||
property bool revealSticky: false
|
||||
|
||||
readonly property bool shouldHideForWindows: {
|
||||
@@ -659,6 +660,7 @@ Variants {
|
||||
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
||||
|
||||
contextMenu: dockVariants.contextMenu
|
||||
trashContextMenu: dockVariants.trashContextMenu
|
||||
groupByApp: dock.groupByApp
|
||||
isVertical: dock.isVertical
|
||||
dockScreen: dock.screen
|
||||
|
||||
@@ -8,6 +8,7 @@ Item {
|
||||
id: root
|
||||
|
||||
property var contextMenu: null
|
||||
property var trashContextMenu: null
|
||||
property bool requestDockShow: false
|
||||
property int pinnedAppCount: 0
|
||||
property bool groupByApp: false
|
||||
@@ -460,19 +461,32 @@ Item {
|
||||
|
||||
function updateModel() {
|
||||
const baseResult = buildBaseItems();
|
||||
dockItems = applyOverflow(baseResult);
|
||||
let finalItems = applyOverflow(baseResult);
|
||||
if (SettingsData.dockShowTrash) {
|
||||
finalItems.push({
|
||||
uniqueKey: "trash_button",
|
||||
type: "trash",
|
||||
appId: "__TRASH__",
|
||||
toplevel: null,
|
||||
isPinned: false,
|
||||
isRunning: false,
|
||||
isInOverflow: false
|
||||
});
|
||||
}
|
||||
dockItems = finalItems;
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: delegateItem
|
||||
|
||||
property var dockButton: itemData.type === "launcher" ? launcherButton : button
|
||||
property var dockButton: itemData.type === "launcher" ? launcherButton : (itemData.type === "trash" ? trashButton : button)
|
||||
property var itemData: modelData
|
||||
readonly property bool isOverflowToggle: itemData.type === "overflow-toggle"
|
||||
readonly property bool isTrash: itemData.type === "trash"
|
||||
readonly property bool isInOverflow: itemData.isInOverflow === true
|
||||
|
||||
clip: false
|
||||
z: (itemData.type === "launcher" ? launcherButton.dragging : button.dragging) ? 100 : 0
|
||||
z: (itemData.type === "launcher" ? launcherButton.dragging : (itemData.type === "trash" ? false : button.dragging)) ? 100 : 0
|
||||
visible: !isInOverflow || root.overflowExpanded
|
||||
opacity: (isInOverflow && !root.overflowExpanded) ? 0 : 1
|
||||
scale: (isInOverflow && !root.overflowExpanded) ? 0.8 : 1
|
||||
@@ -568,9 +582,21 @@ Item {
|
||||
index: model.index
|
||||
}
|
||||
|
||||
DockTrashButton {
|
||||
id: trashButton
|
||||
visible: itemData.type === "trash"
|
||||
anchors.centerIn: parent
|
||||
width: delegateItem.width
|
||||
height: delegateItem.height
|
||||
actualIconSize: root.iconSize
|
||||
dockApps: root
|
||||
contextMenu: root.trashContextMenu
|
||||
parentDockScreen: root.dockScreen
|
||||
}
|
||||
|
||||
DockAppButton {
|
||||
id: button
|
||||
visible: !isOverflowToggle && itemData.type !== "separator" && itemData.type !== "launcher"
|
||||
visible: !isOverflowToggle && itemData.type !== "separator" && itemData.type !== "launcher" && itemData.type !== "trash"
|
||||
anchors.centerIn: parent
|
||||
width: delegateItem.width
|
||||
height: delegateItem.height
|
||||
@@ -640,6 +666,9 @@ Item {
|
||||
function onDockMaxVisibleRunningAppsChanged() {
|
||||
repeater.updateModel();
|
||||
}
|
||||
function onDockShowTrashChanged() {
|
||||
repeater.updateModel();
|
||||
}
|
||||
}
|
||||
|
||||
onGroupByAppChanged: repeater.updateModel()
|
||||
|
||||
137
quickshell/Modules/Dock/DockTrashButton.qml
Normal file
137
quickshell/Modules/Dock/DockTrashButton.qml
Normal file
@@ -0,0 +1,137 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
clip: false
|
||||
|
||||
property var dockApps: null
|
||||
property var contextMenu: null
|
||||
property var parentDockScreen: null
|
||||
property real actualIconSize: 40
|
||||
property real hoverAnimOffset: 0
|
||||
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
property bool showTooltip: mouseArea.containsMouse
|
||||
readonly property string tooltipText: TrashService.isEmpty ? I18n.tr("Trash") : (I18n.tr("Trash") + " (" + TrashService.count + ")")
|
||||
|
||||
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
|
||||
readonly property real animationDistance: actualIconSize
|
||||
readonly property real animationDirection: {
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
|
||||
return -1;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Top)
|
||||
return 1;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right)
|
||||
return -1;
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Left)
|
||||
return 1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
onIsHoveredChanged: {
|
||||
if (mouseArea.pressed)
|
||||
return;
|
||||
if (isHovered) {
|
||||
exitAnimation.stop();
|
||||
if (!bounceAnimation.running)
|
||||
bounceAnimation.restart();
|
||||
} else {
|
||||
bounceAnimation.stop();
|
||||
exitAnimation.restart();
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: bounceAnimation
|
||||
|
||||
running: false
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "hoverAnimOffset"
|
||||
to: animationDirection * animationDistance * 0.25
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasizedAccel
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "hoverAnimOffset"
|
||||
to: animationDirection * animationDistance * 0.2
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasizedDecel
|
||||
}
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: exitAnimation
|
||||
|
||||
running: false
|
||||
target: root
|
||||
property: "hoverAnimOffset"
|
||||
to: 0
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasizedDecel
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
TrashService.openTrash();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (contextMenu) {
|
||||
contextMenu.showForButton(root, root.height, parentDockScreen, dockApps);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: visualContent
|
||||
anchors.fill: parent
|
||||
|
||||
transform: Translate {
|
||||
x: !isVertical ? 0 : hoverAnimOffset
|
||||
y: !isVertical ? hoverAnimOffset : 0
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
width: actualIconSize
|
||||
height: actualIconSize
|
||||
|
||||
IconImage {
|
||||
id: trashIcon
|
||||
anchors.centerIn: parent
|
||||
width: actualIconSize - 4
|
||||
height: actualIconSize - 4
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
source: Quickshell.iconPath(TrashService.isEmpty ? "user-trash" : "user-trash-full", "user-trash")
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
377
quickshell/Modules/Dock/DockTrashContextMenu.qml
Normal file
377
quickshell/Modules/Dock/DockTrashContextMenu.qml
Normal file
@@ -0,0 +1,377 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
WindowBlur {
|
||||
targetWindow: root
|
||||
blurX: menuContainer.x
|
||||
blurY: menuContainer.y
|
||||
blurWidth: root.visible ? menuContainer.width : 0
|
||||
blurHeight: root.visible ? menuContainer.height : 0
|
||||
blurRadius: Theme.cornerRadius
|
||||
}
|
||||
|
||||
WlrLayershell.namespace: "dms:dock-trash-context-menu"
|
||||
|
||||
property var anchorItem: null
|
||||
property real dockVisibleHeight: 40
|
||||
property int margin: 10
|
||||
property var dockApps: null
|
||||
|
||||
function showForButton(button, dockHeight, dockScreen, parentDockApps) {
|
||||
if (dockScreen) {
|
||||
root.screen = dockScreen;
|
||||
}
|
||||
|
||||
anchorItem = button;
|
||||
dockVisibleHeight = dockHeight || 40;
|
||||
dockApps = parentDockApps || null;
|
||||
|
||||
visible = true;
|
||||
}
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
screen: null
|
||||
visible: false
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
property point anchorPos: Qt.point(screen ? screen.width / 2 : 0, screen ? screen.height - 100 : 0)
|
||||
|
||||
onAnchorItemChanged: updatePosition()
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
updatePosition();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
if (!anchorItem || !screen) {
|
||||
anchorPos = Qt.point(screen ? screen.width / 2 : 0, screen ? screen.height - 100 : 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const dockWindow = anchorItem.Window.window;
|
||||
if (!dockWindow) {
|
||||
anchorPos = Qt.point(screen.width / 2, screen.height - 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0);
|
||||
let actualDockHeight = root.dockVisibleHeight;
|
||||
|
||||
function findDockBackground(item) {
|
||||
if (item.objectName === "dockBackground") {
|
||||
return item;
|
||||
}
|
||||
for (var i = 0; i < item.children.length; i++) {
|
||||
const found = findDockBackground(item.children[i]);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const dockBackground = findDockBackground(dockWindow.contentItem);
|
||||
let actualDockWidth = dockWindow.width;
|
||||
if (dockBackground) {
|
||||
actualDockHeight = dockBackground.height;
|
||||
actualDockWidth = dockBackground.width;
|
||||
}
|
||||
|
||||
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right;
|
||||
const dockMargin = SettingsData.dockMargin + 16;
|
||||
let buttonScreenX, buttonScreenY;
|
||||
|
||||
if (isVertical) {
|
||||
const dockContentHeight = dockWindow.height;
|
||||
const screenHeight = root.screen.height;
|
||||
const dockTopMargin = Math.round((screenHeight - dockContentHeight) / 2);
|
||||
buttonScreenY = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2;
|
||||
|
||||
if (SettingsData.dockPosition === SettingsData.Position.Right) {
|
||||
buttonScreenX = root.screen.width - actualDockWidth - dockMargin - 20;
|
||||
} else {
|
||||
buttonScreenX = actualDockWidth + dockMargin + 20;
|
||||
}
|
||||
} else {
|
||||
const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom;
|
||||
|
||||
if (isDockAtBottom) {
|
||||
buttonScreenY = root.screen.height - actualDockHeight - dockMargin - 20;
|
||||
} else {
|
||||
buttonScreenY = actualDockHeight + dockMargin + 20;
|
||||
}
|
||||
|
||||
const dockContentWidth = dockWindow.width;
|
||||
const screenWidth = root.screen.width;
|
||||
const dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2);
|
||||
buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2;
|
||||
}
|
||||
|
||||
anchorPos = Qt.point(buttonScreenX, buttonScreenY);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: menuContainer
|
||||
|
||||
x: {
|
||||
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right;
|
||||
if (isVertical) {
|
||||
const isDockAtRight = SettingsData.dockPosition === SettingsData.Position.Right;
|
||||
if (isDockAtRight) {
|
||||
return Math.max(10, root.anchorPos.x - width + 30);
|
||||
} else {
|
||||
return Math.min(root.width - width - 10, root.anchorPos.x - 30);
|
||||
}
|
||||
} else {
|
||||
const left = 10;
|
||||
const right = root.width - width - 10;
|
||||
const want = root.anchorPos.x - width / 2;
|
||||
return Math.max(left, Math.min(right, want));
|
||||
}
|
||||
}
|
||||
y: {
|
||||
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right;
|
||||
if (isVertical) {
|
||||
const top = 10;
|
||||
const bottom = root.height - height - 10;
|
||||
const want = root.anchorPos.y - height / 2;
|
||||
return Math.max(top, Math.min(bottom, want));
|
||||
} else {
|
||||
const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom;
|
||||
if (isDockAtBottom) {
|
||||
return Math.max(10, root.anchorPos.y - height + 30);
|
||||
} else {
|
||||
return Math.min(root.height - height - 10, root.anchorPos.y - 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
width: Math.min(400, Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2))
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: BlurService.enabled ? BlurService.borderWidth : 1
|
||||
|
||||
opacity: root.visible ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: -1
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: openArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: "folder_open"
|
||||
size: 14
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("Open Trash")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: openRipple
|
||||
rippleColor: Theme.surfaceText
|
||||
cornerRadius: Theme.cornerRadius
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: openArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => openRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: {
|
||||
TrashService.openTrash();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
enabled: !TrashService.isEmpty
|
||||
opacity: enabled ? 1 : 0.4
|
||||
color: emptyArea.containsMouse && enabled ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: "delete_forever"
|
||||
size: 14
|
||||
color: emptyArea.containsMouse && parent.parent.enabled ? Theme.error : Theme.surfaceText
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: TrashService.isEmpty ? I18n.tr("Empty Trash") : I18n.tr("Empty Trash (%1)").arg(TrashService.count)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: emptyArea.containsMouse && parent.parent.enabled ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: emptyRipple
|
||||
rippleColor: Theme.error
|
||||
cornerRadius: Theme.cornerRadius
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: emptyArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: parent.enabled
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => emptyRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: {
|
||||
TrashService.requestEmptyTrash();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: settingsArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: "settings"
|
||||
size: 14
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: I18n.tr("Settings")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: settingsRipple
|
||||
rippleColor: Theme.surfaceText
|
||||
cornerRadius: Theme.cornerRadius
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: settingsArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => settingsRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: {
|
||||
PopoutService.focusOrToggleSettingsWithTab("dock");
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
@@ -506,6 +506,79 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "delete"
|
||||
title: I18n.tr("Trash")
|
||||
settingKey: "dockTrash"
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dockShowTrash"
|
||||
tags: ["dock", "trash", "bin", "recycle"]
|
||||
text: I18n.tr("Show Trash in Dock")
|
||||
description: I18n.tr("Place a trash bin at the end of the dock")
|
||||
checked: SettingsData.dockShowTrash
|
||||
onToggled: checked => SettingsData.set("dockShowTrash", checked)
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
id: trashFmDropdown
|
||||
settingKey: "dockTrashFileManager"
|
||||
tags: ["dock", "trash", "file", "manager", "nautilus", "thunar", "dolphin", "custom"]
|
||||
text: I18n.tr("Open Trash With")
|
||||
description: I18n.tr("File manager used to open the trash. Pick \"custom\" to enter your own command.")
|
||||
visible: SettingsData.dockShowTrash
|
||||
currentValue: SettingsData.dockTrashFileManager
|
||||
options: TrashService.availableFileManagers || []
|
||||
onValueChanged: value => SettingsData.set("dockTrashFileManager", value)
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
height: visible ? trashCustomCommandColumn.implicitHeight : 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
visible: SettingsData.dockShowTrash && SettingsData.dockTrashFileManager === "custom"
|
||||
|
||||
Column {
|
||||
id: trashCustomCommandColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Custom open-trash command")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: trashCustomCommandField
|
||||
width: parent.width
|
||||
placeholderText: "pcmanfm trash:///"
|
||||
backgroundColor: Theme.surfaceContainerHighest
|
||||
normalBorderColor: Theme.outlineMedium
|
||||
focusedBorderColor: Theme.primary
|
||||
|
||||
Component.onCompleted: {
|
||||
if (SettingsData.dockTrashCustomCommand) {
|
||||
text = SettingsData.dockTrashCustomCommand;
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: SettingsData.set("dockTrashCustomCommand", text.trim())
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => {
|
||||
trashCustomCommandField.forceActiveFocus();
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "photo_size_select_large"
|
||||
|
||||
92
quickshell/Services/TrashService.qml
Normal file
92
quickshell/Services/TrashService.qml
Normal file
@@ -0,0 +1,92 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.folderlistmodel
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string _homeDir: Quickshell.env("HOME") || ""
|
||||
readonly property string _xdgDataHome: Quickshell.env("XDG_DATA_HOME") || (_homeDir + "/.local/share")
|
||||
readonly property string trashFilesDir: _xdgDataHome + "/Trash/files"
|
||||
|
||||
readonly property int count: trashModel.count
|
||||
readonly property bool isEmpty: count === 0
|
||||
|
||||
property var availableFileManagers: []
|
||||
|
||||
signal emptyTrashConfirmRequested(int itemCount)
|
||||
|
||||
FolderListModel {
|
||||
id: trashModel
|
||||
folder: "file://" + root.trashFilesDir
|
||||
showDirs: true
|
||||
showFiles: true
|
||||
showHidden: true
|
||||
showDotAndDotDot: false
|
||||
sortField: FolderListModel.Name
|
||||
nameFilters: ["*"]
|
||||
}
|
||||
|
||||
Process {
|
||||
id: detectProc
|
||||
running: false
|
||||
command: ["sh", "-c", "for fm in nautilus thunar dolphin; do command -v $fm >/dev/null 2>&1 && echo $fm; done"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const detected = (text || "").split("\n").map(s => s.trim()).filter(s => s.length > 0);
|
||||
detected.push("custom");
|
||||
root.availableFileManagers = detected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
detectProc.running = true;
|
||||
}
|
||||
|
||||
function openTrash() {
|
||||
const choice = SettingsData.dockTrashFileManager || "nautilus";
|
||||
if (choice === "custom") {
|
||||
const cmd = (SettingsData.dockTrashCustomCommand || "").trim();
|
||||
if (!cmd) {
|
||||
ToastService.showInfo(I18n.tr("Cannot open trash: no custom command set"), I18n.tr("Configure one in Settings → Dock → Trash."));
|
||||
return;
|
||||
}
|
||||
Proc.runCommand(null, ["sh", "-c", cmd], (output, exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
ToastService.showError(I18n.tr("Trash command failed (exit %1)").arg(exitCode), I18n.tr("Check your custom command in Settings → Dock → Trash."));
|
||||
}
|
||||
}, 0, Proc.noTimeout);
|
||||
return;
|
||||
}
|
||||
if (availableFileManagers.indexOf(choice) < 0) {
|
||||
ToastService.showInfo(I18n.tr("Cannot open trash: '%1' is not installed").arg(choice), I18n.tr("Pick a different file manager in Settings → Dock → Trash."));
|
||||
return;
|
||||
}
|
||||
switch (choice) {
|
||||
case "nautilus":
|
||||
Quickshell.execDetached(["nautilus", "trash:///"]);
|
||||
break;
|
||||
case "thunar":
|
||||
Quickshell.execDetached(["thunar", "trash:///"]);
|
||||
break;
|
||||
case "dolphin":
|
||||
Quickshell.execDetached(["dolphin", "trash:///"]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function requestEmptyTrash() {
|
||||
if (isEmpty)
|
||||
return;
|
||||
emptyTrashConfirmRequested(count);
|
||||
}
|
||||
|
||||
function emptyTrash() {
|
||||
Quickshell.execDetached(["gio", "trash", "--empty"]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user