Files
DankMaterialShell/quickshell/Modules/DesktopContextMenu.qml
T

281 lines
10 KiB
QML

pragma ComponentBehavior: Bound
import Qt.labs.folderlistmodel
import QtQuick
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.DCommon.Widgets
import qs.Widgets
import "../DCommon/Common/WheelInput.js" as WheelInput
DContextMenu {
id: root
property bool settingsExpanded: false
property var stripPaths: []
readonly property int stripCount: 4
readonly property real stripUnit: Theme.listItemHeight
readonly property string currentWallpaper: {
if (SessionData.perMonitorWallpaper && targetScreen)
return SessionData.getMonitorWallpaper(targetScreen.name) || "";
return SessionData.wallpaperPath || "";
}
readonly property string wallpaperDir: currentWallpaper.startsWith("/") ? currentWallpaper.substring(0, currentWallpaper.lastIndexOf("/")) : ""
layerNamespace: "dms:desktop-context-menu"
keyboardNavigable: true
itemHeight: Theme.listItemHeight
itemRadius: Theme.cornerRadiusFull
itemFontSize: Theme.fontSizeMedium
containerRadius: Theme.cornerRadiusXL
minMenuWidth: stripUnit * (stripCount + 1) + Theme.spacingS * (stripCount + 1)
menuItems: {
const items = [
{
type: "item",
icon: "add",
text: I18n.tr("Add widget"),
action: () => DesktopWidgetRegistry.startEditing(root.targetScreen, true)
},
{
type: "item",
icon: "edit",
text: I18n.tr("Edit widgets"),
action: () => DesktopWidgetRegistry.startEditing(root.targetScreen, false)
},
{
type: "item",
icon: "palette",
text: I18n.tr("Wallpaper & colors"),
action: () => PopoutService.openSettingsWithTab("personalization")
}
];
if (wallpaperDir)
items.push({
type: "component",
component: wallpaperStrip,
height: stripUnit * 2 + Theme.spacingS
});
return items.concat([settingsEntry, allSettingsEntry, desktopWidgetsEntry, displaysEntry, pluginsEntry]);
}
// Stable objects so expanding Settings doesn't rebuild every row.
component MenuEntry: QtObject {
property string type: "item"
property string icon
property string text
property string trailingIcon
property bool keepOpen
property bool hidden
property var action
}
MenuEntry {
id: settingsEntry
icon: "settings"
text: I18n.tr("Settings")
trailingIcon: root.settingsExpanded ? "expand_less" : "expand_more"
keepOpen: true
action: () => root.settingsExpanded = !root.settingsExpanded
}
MenuEntry {
id: allSettingsEntry
icon: "tune"
text: I18n.tr("All settings")
hidden: !root.settingsExpanded
action: () => PopoutService.openSettings()
}
MenuEntry {
id: desktopWidgetsEntry
icon: "widgets"
text: I18n.tr("Desktop widgets")
hidden: !root.settingsExpanded
action: () => PopoutService.openSettingsWithTab("desktop_widgets")
}
MenuEntry {
id: displaysEntry
icon: "monitor"
text: I18n.tr("Displays")
hidden: !root.settingsExpanded
action: () => PopoutService.openSettingsWithTab("displays")
}
MenuEntry {
id: pluginsEntry
icon: "extension"
text: I18n.tr("Plugins")
hidden: !root.settingsExpanded
action: () => PopoutService.openSettingsWithTab("plugins")
}
onBackdropRightClicked: (x, y) => open(targetScreen, x, y, false)
onRenderActiveChanged: {
if (renderActive)
return;
settingsExpanded = false;
stripPaths = [];
}
onWallpaperDirChanged: stripPaths = []
function setWallpaper(path) {
if (SessionData.perMonitorWallpaper && targetScreen) {
SessionData.setMonitorWallpaper(targetScreen.name, path);
SessionData.setMonitorCyclingFolderPath(targetScreen.name, "");
return;
}
SessionData.setWallpaper(path);
SessionData.wallpaperCyclingFolderPath = "";
SessionData.saveSettings();
}
// Current wallpaper leads; the order is fixed per open so a pick moves the check, not the list.
function loadStrip(model) {
const paths = [];
for (let i = 0; i < model.count; i++)
paths.push(model.get(i, "filePath"));
const start = Math.max(0, paths.indexOf(currentWallpaper));
stripPaths = paths.slice(start).concat(paths.slice(0, start));
}
Loader {
active: root.renderActive && root.wallpaperDir !== ""
sourceComponent: Item {
FolderListModel {
id: folderModel
showDirs: false
showDotAndDotDot: false
caseSensitive: false
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr", "*.svg"]
folder: "file://" + root.wallpaperDir.split("/").map(s => encodeURIComponent(s)).join("/")
onStatusChanged: {
if (folderModel.status === FolderListModel.Ready)
root.loadStrip(folderModel);
}
}
}
}
Component {
id: wallpaperStrip
Item {
// Not a Dank* wrapper: DListView only scrolls vertically.
ListView {
id: strip
readonly property real unit: (width - spacing * (root.stripCount - 1)) / (root.stripCount + 1)
function scrollBy(wheel) {
const delta = WheelInput.isTouchpad(wheel) ? WheelInput.dominantDelta(wheel.pixelDelta) : WheelInput.dominantDelta(wheel.angleDelta) / 120 * (unit + spacing);
const maxX = originX + Math.max(0, contentWidth - width);
contentX = Math.max(originX, Math.min(maxX, contentX - delta));
}
anchors.fill: parent
orientation: ListView.Horizontal
spacing: Theme.spacingS
clip: true
boundsBehavior: Flickable.StopAtBounds
model: root.stripPaths
delegate: Item {
id: thumb
required property string modelData
readonly property bool current: thumb.modelData === root.currentWallpaper
y: Theme.spacingXS
width: strip.unit * (thumb.current ? 2 : 1)
height: root.stripUnit * 2
Behavior on width {
NumberAnimation {
duration: SettingsData.reduceMotion ? 0 : Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
ClippingRectangle {
id: thumbClip
anchors.fill: parent
radius: thumb.current ? Theme.cornerRadiusXL : Math.min(Theme.cornerRadiusFull, width / 2)
color: Theme.surfaceContainerHigh
Behavior on radius {
NumberAnimation {
duration: SettingsData.reduceMotion ? 0 : Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
CachingImage {
anchors.fill: parent
imagePath: thumb.modelData
maxCacheSize: 256
animate: false
}
Rectangle {
anchors.fill: parent
color: Theme.withAlpha(Theme.onSurface, thumbMouse.pressed ? Theme.stateLayerPressed : thumbMouse.containsMouse ? Theme.stateLayerHover : 0)
}
}
Rectangle {
anchors.centerIn: parent
width: Theme.iconSizeLarge
height: width
radius: width / 2
color: Theme.primaryContainer
scale: thumb.current ? 1 : 0
visible: scale > 0
Behavior on scale {
NumberAnimation {
duration: SettingsData.reduceMotion ? 0 : Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
DIcon {
anchors.centerIn: parent
name: "check"
size: Theme.iconSizeMedium
color: Theme.onPrimaryContainer
}
}
MouseArea {
id: thumbMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!thumb.current)
root.setWallpaper(thumb.modelData);
}
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: wheel => {
strip.scrollBy(wheel);
wheel.accepted = true;
}
}
}
}
}