mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-02 02: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 dockMaxVisibleApps: 0
|
||||||
property int dockMaxVisibleRunningApps: 0
|
property int dockMaxVisibleRunningApps: 0
|
||||||
property bool dockShowOverflowBadge: true
|
property bool dockShowOverflowBadge: true
|
||||||
|
property bool dockShowTrash: false
|
||||||
|
property string dockTrashFileManager: "nautilus"
|
||||||
|
property string dockTrashCustomCommand: ""
|
||||||
|
|
||||||
property bool notificationOverlayEnabled: false
|
property bool notificationOverlayEnabled: false
|
||||||
property bool notificationPopupShadowEnabled: true
|
property bool notificationPopupShadowEnabled: true
|
||||||
|
|||||||
@@ -350,6 +350,9 @@ var SPEC = {
|
|||||||
dockMaxVisibleApps: { def: 0 },
|
dockMaxVisibleApps: { def: 0 },
|
||||||
dockMaxVisibleRunningApps: { def: 0 },
|
dockMaxVisibleRunningApps: { def: 0 },
|
||||||
dockShowOverflowBadge: { def: true },
|
dockShowOverflowBadge: { def: true },
|
||||||
|
dockShowTrash: { def: false },
|
||||||
|
dockTrashFileManager: { def: "nautilus" },
|
||||||
|
dockTrashCustomCommand: { def: "" },
|
||||||
|
|
||||||
notificationOverlayEnabled: { def: false },
|
notificationOverlayEnabled: { def: false },
|
||||||
notificationPopupShadowEnabled: { def: true },
|
notificationPopupShadowEnabled: { def: true },
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import qs.Common
|
|||||||
import qs.Modals
|
import qs.Modals
|
||||||
import qs.Modals.Changelog
|
import qs.Modals.Changelog
|
||||||
import qs.Modals.Clipboard
|
import qs.Modals.Clipboard
|
||||||
|
import qs.Modals.Common
|
||||||
import qs.Modals.Greeter
|
import qs.Modals.Greeter
|
||||||
import qs.Modals.Settings
|
import qs.Modals.Settings
|
||||||
import qs.Modals.DankLauncherV2
|
import qs.Modals.DankLauncherV2
|
||||||
@@ -284,11 +285,15 @@ Item {
|
|||||||
|
|
||||||
sourceComponent: Dock {
|
sourceComponent: Dock {
|
||||||
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
|
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
|
||||||
|
trashContextMenu: dockTrashContextMenuLoader.item ? dockTrashContextMenuLoader.item : null
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoaded: {
|
onLoaded: {
|
||||||
if (item) {
|
if (item) {
|
||||||
dockContextMenuLoader.active = true;
|
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 {
|
LazyLoader {
|
||||||
id: notificationCenterLoader
|
id: notificationCenterLoader
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Variants {
|
|||||||
model: SettingsData.getFilteredScreens("dock")
|
model: SettingsData.getFilteredScreens("dock")
|
||||||
|
|
||||||
property var contextMenu
|
property var contextMenu
|
||||||
|
property var trashContextMenu
|
||||||
|
|
||||||
delegate: PanelWindow {
|
delegate: PanelWindow {
|
||||||
id: dock
|
id: dock
|
||||||
@@ -120,7 +121,7 @@ Variants {
|
|||||||
return Math.round(v * _dpr) / _dpr;
|
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
|
property bool revealSticky: false
|
||||||
|
|
||||||
readonly property bool shouldHideForWindows: {
|
readonly property bool shouldHideForWindows: {
|
||||||
@@ -659,6 +660,7 @@ Variants {
|
|||||||
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
|
||||||
|
|
||||||
contextMenu: dockVariants.contextMenu
|
contextMenu: dockVariants.contextMenu
|
||||||
|
trashContextMenu: dockVariants.trashContextMenu
|
||||||
groupByApp: dock.groupByApp
|
groupByApp: dock.groupByApp
|
||||||
isVertical: dock.isVertical
|
isVertical: dock.isVertical
|
||||||
dockScreen: dock.screen
|
dockScreen: dock.screen
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Item {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var contextMenu: null
|
property var contextMenu: null
|
||||||
|
property var trashContextMenu: null
|
||||||
property bool requestDockShow: false
|
property bool requestDockShow: false
|
||||||
property int pinnedAppCount: 0
|
property int pinnedAppCount: 0
|
||||||
property bool groupByApp: false
|
property bool groupByApp: false
|
||||||
@@ -460,19 +461,32 @@ Item {
|
|||||||
|
|
||||||
function updateModel() {
|
function updateModel() {
|
||||||
const baseResult = buildBaseItems();
|
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 {
|
delegate: Item {
|
||||||
id: delegateItem
|
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
|
property var itemData: modelData
|
||||||
readonly property bool isOverflowToggle: itemData.type === "overflow-toggle"
|
readonly property bool isOverflowToggle: itemData.type === "overflow-toggle"
|
||||||
|
readonly property bool isTrash: itemData.type === "trash"
|
||||||
readonly property bool isInOverflow: itemData.isInOverflow === true
|
readonly property bool isInOverflow: itemData.isInOverflow === true
|
||||||
|
|
||||||
clip: false
|
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
|
visible: !isInOverflow || root.overflowExpanded
|
||||||
opacity: (isInOverflow && !root.overflowExpanded) ? 0 : 1
|
opacity: (isInOverflow && !root.overflowExpanded) ? 0 : 1
|
||||||
scale: (isInOverflow && !root.overflowExpanded) ? 0.8 : 1
|
scale: (isInOverflow && !root.overflowExpanded) ? 0.8 : 1
|
||||||
@@ -568,9 +582,21 @@ Item {
|
|||||||
index: model.index
|
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 {
|
DockAppButton {
|
||||||
id: button
|
id: button
|
||||||
visible: !isOverflowToggle && itemData.type !== "separator" && itemData.type !== "launcher"
|
visible: !isOverflowToggle && itemData.type !== "separator" && itemData.type !== "launcher" && itemData.type !== "trash"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
width: delegateItem.width
|
width: delegateItem.width
|
||||||
height: delegateItem.height
|
height: delegateItem.height
|
||||||
@@ -640,6 +666,9 @@ Item {
|
|||||||
function onDockMaxVisibleRunningAppsChanged() {
|
function onDockMaxVisibleRunningAppsChanged() {
|
||||||
repeater.updateModel();
|
repeater.updateModel();
|
||||||
}
|
}
|
||||||
|
function onDockShowTrashChanged() {
|
||||||
|
repeater.updateModel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupByAppChanged: 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 {
|
SettingsCard {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
iconName: "photo_size_select_large"
|
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