This commit is contained in:
Zacharias-Brohn
2026-03-12 10:04:27 +01:00
parent 401ccef90c
commit 851b78f0ff
17 changed files with 1347 additions and 64 deletions
+201
View File
@@ -0,0 +1,201 @@
pragma Singleton
import Quickshell
Singleton {
id: root
function getAppId(fileName) {
return fileName.endsWith(".desktop") ? fileName.replace(".desktop", "") : null;
}
function getFileType(fileName, isDir) {
if (isDir)
return "directory";
let ext = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : "";
if (ext === "desktop")
return "desktop";
const map = {
"image": ["png", "jpg", "jpeg", "svg", "gif", "bmp", "webp", "ico", "tiff", "tif", "heic", "heif", "raw", "psd", "ai", "xcf"],
"video": ["mp4", "mkv", "webm", "avi", "mov", "flv", "wmv", "m4v", "mpg", "mpeg", "3gp", "vob", "ogv", "ts"],
"audio": ["mp3", "wav", "flac", "aac", "ogg", "m4a", "wma", "opus", "alac", "mid", "midi", "amr"],
"archive": ["zip", "tar", "gz", "rar", "7z", "xz", "bz2", "tgz", "iso", "img", "dmg", "deb", "rpm", "apk"],
"document": ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "ods", "odp", "rtf", "epub", "mobi", "djvu"],
"text": ["txt", "md", "rst", "tex", "log", "json", "xml", "yaml", "yml", "toml", "ini", "conf", "cfg", "env", "csv", "tsv"],
"code": ["qml", "cpp", "c", "h", "hpp", "py", "js", "ts", "jsx", "tsx", "java", "rs", "go", "rb", "php", "cs", "swift", "kt", "sh", "bash", "zsh", "fish", "html", "htm", "css", "scss", "sass", "less", "vue", "svelte", "sql", "graphql", "lua", "pl", "dart", "r", "dockerfile", "make"],
"executable": ["exe", "msi", "bat", "cmd", "appimage", "run", "bin", "out", "so", "dll"],
"font": ["ttf", "otf", "woff", "woff2"]
};
for (const [type, extensions] of Object.entries(map)) {
if (extensions.includes(ext))
return type;
}
return "unknown";
}
function getIconName(fileName, isDir) {
if (isDir)
return "folder";
let ext = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : "";
const map = {
// Images
"png": "image-x-generic",
"jpg": "image-x-generic",
"jpeg": "image-x-generic",
"svg": "image-svg+xml",
"gif": "image-x-generic",
"bmp": "image-x-generic",
"webp": "image-x-generic",
"ico": "image-x-generic",
"tiff": "image-x-generic",
"tif": "image-x-generic",
"heic": "image-x-generic",
"heif": "image-x-generic",
"raw": "image-x-generic",
"psd": "image-vnd.adobe.photoshop",
"ai": "application-illustrator",
"xcf": "image-x-xcf",
// Vidéos
"mp4": "video-x-generic",
"mkv": "video-x-generic",
"webm": "video-x-generic",
"avi": "video-x-generic",
"mov": "video-x-generic",
"flv": "video-x-generic",
"wmv": "video-x-generic",
"m4v": "video-x-generic",
"mpg": "video-x-generic",
"mpeg": "video-x-generic",
"3gp": "video-x-generic",
"vob": "video-x-generic",
"ogv": "video-x-generic",
"ts": "video-x-generic",
// Audio
"mp3": "audio-x-generic",
"wav": "audio-x-generic",
"flac": "audio-x-generic",
"aac": "audio-x-generic",
"ogg": "audio-x-generic",
"m4a": "audio-x-generic",
"wma": "audio-x-generic",
"opus": "audio-x-generic",
"alac": "audio-x-generic",
"mid": "audio-midi",
"midi": "audio-midi",
"amr": "audio-x-generic",
// Archives & Images
"zip": "application-zip",
"tar": "application-x-tar",
"gz": "application-gzip",
"rar": "application-vnd.rar",
"7z": "application-x-7z-compressed",
"xz": "application-x-xz",
"bz2": "application-x-bzip2",
"tgz": "application-x-compressed-tar",
"iso": "application-x-cd-image",
"img": "application-x-cd-image",
"dmg": "application-x-apple-diskimage",
"deb": "application-vnd.debian.binary-package",
"rpm": "application-x-rpm",
"apk": "application-vnd.android.package-archive",
// Documents
"pdf": "application-pdf",
"doc": "application-msword",
"docx": "application-vnd.openxmlformats-officedocument.wordprocessingml.document",
"xls": "application-vnd.ms-excel",
"xlsx": "application-vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"ppt": "application-vnd.ms-powerpoint",
"pptx": "application-vnd.openxmlformats-officedocument.presentationml.presentation",
"odt": "application-vnd.oasis.opendocument.text",
"ods": "application-vnd.oasis.opendocument.spreadsheet",
"odp": "application-vnd.oasis.opendocument.presentation",
"rtf": "application-rtf",
"epub": "application-epub+zip",
"mobi": "application-x-mobipocket-ebook",
"djvu": "image-vnd.djvu",
"csv": "text-csv",
"tsv": "text-tab-separated-values",
// Data & Config
"txt": "text-x-generic",
"md": "text-markdown",
"rst": "text-x-rst",
"tex": "text-x-tex",
"log": "text-x-log",
"json": "application-json",
"xml": "text-xml",
"yaml": "text-x-yaml",
"yml": "text-x-yaml",
"toml": "text-x-toml",
"ini": "text-x-generic",
"conf": "text-x-generic",
"cfg": "text-x-generic",
"env": "text-x-generic",
// Code
"qml": "text-x-qml",
"cpp": "text-x-c++src",
"c": "text-x-csrc",
"h": "text-x-chdr",
"hpp": "text-x-c++hdr",
"py": "text-x-python",
"js": "text-x-javascript",
"ts": "text-x-typescript",
"jsx": "text-x-javascript",
"tsx": "text-x-typescript",
"java": "text-x-java",
"rs": "text-x-rust",
"go": "text-x-go",
"rb": "text-x-ruby",
"php": "application-x-php",
"cs": "text-x-csharp",
"swift": "text-x-swift",
"kt": "text-x-kotlin",
"sh": "application-x-shellscript",
"bash": "application-x-shellscript",
"zsh": "application-x-shellscript",
"fish": "application-x-shellscript",
"html": "text-html",
"htm": "text-html",
"css": "text-css",
"scss": "text-x-scss",
"sass": "text-x-sass",
"less": "text-x-less",
"vue": "text-html",
"svelte": "text-html",
"sql": "application-x-sql",
"graphql": "text-x-generic",
"lua": "text-x-lua",
"pl": "text-x-perl",
"dart": "text-x-dart",
"r": "text-x-r",
"dockerfile": "text-x-generic",
"make": "text-x-makefile",
// Executables
"exe": "application-x-executable",
"msi": "application-x-msi",
"bat": "application-x-ms-dos-executable",
"cmd": "application-x-ms-dos-executable",
"appimage": "application-x-executable",
"run": "application-x-executable",
"bin": "application-x-executable",
"out": "application-x-executable",
"so": "application-x-sharedlib",
"dll": "application-x-sharedlib",
// Fonts
"ttf": "font-x-generic",
"otf": "font-x-generic",
"woff": "font-x-generic",
"woff2": "font-x-generic"
};
return map[ext] || "text-x-generic";
}
}
+28
View File
@@ -0,0 +1,28 @@
pragma Singleton
import Quickshell
Singleton {
id: root
function fileNameForPath(str) {
if (typeof str !== "string")
return "";
const trimmed = trimFileProtocol(str);
return trimmed.split(/[\\/]/).pop();
}
function trimFileExt(str) {
if (typeof str !== "string")
return "";
const trimmed = trimFileProtocol(str);
const lastDot = trimmed.lastIndexOf(".");
if (lastDot > -1 && lastDot > trimmed.lastIndexOf("/")) {
return trimmed.slice(0, lastDot);
}
return trimmed;
}
function trimFileProtocol(str) {
return str.startsWith("file://") ? str.slice(7) : str;
}
}
@@ -0,0 +1,150 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Hyprland
import qs.Components
import qs.Config
import qs.Paths
Item {
id: root
anchors.fill: parent
z: 998
visible: false
property real menuX: 0
property real menuY: 0
MouseArea {
anchors.fill: parent
onClicked: root.close()
}
CustomClippingRect {
id: popupBackground
readonly property real padding: 4
x: root.menuX
y: root.menuY
color: DynamicColors.tPalette.m3surface
radius: Appearance.rounding.normal
implicitWidth: menuLayout.implicitWidth + padding * 2
implicitHeight: menuLayout.implicitHeight + padding * 2
Behavior on opacity { Anim {} }
opacity: root.visible ? 1 : 0
ColumnLayout {
id: menuLayout
anchors.fill: parent
anchors.margins: popupBackground.padding
spacing: 0
StateLayer {
Layout.fillWidth: true
contentItem: RowLayout {
spacing: 8
anchors.fill: parent
anchors.margins: 12
MaterialIcon { text: "terminal"; font.pointSize: 20 }
CustomText { text: "Open terminal"; Layout.fillWidth: true }
}
onClicked: {
Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", FileUtils.trimFileProtocol(Paths.desktop)])
root.close()
}
}
CustomRect {
Layout.fillWidth: true
implicitHeight: 1
color: DynamicColors.palette.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4
}
StateLayer {
Layout.fillWidth: true
contentItem: RowLayout {
spacing: 8
anchors.fill: parent
anchors.margins: 12
MaterialIcon { text: "settings"; font.pointSize: 20 }
CustomText { text: "Sleex settings"; Layout.fillWidth: true }
}
onClicked: {
Quickshell.execDetached(["qs", "-p", "/usr/share/sleex/settings.qml"])
root.close()
}
}
CustomRect {
Layout.fillWidth: true
implicitHeight: 1
color: Appearance.m3colors.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4
}
StateLayer {
Layout.fillWidth: true
contentItem: RowLayout {
spacing: 8
anchors.fill: parent
anchors.margins: 12
MaterialIcon { text: "logout"; font.pointSize: 20 }
CustomText { text: "Logout"; Layout.fillWidth: true }
}
onClicked: {
Hyprland.dispatch("global quickshell:sessionOpen")
root.close()
}
}
CustomRect {
Layout.fillWidth: true
implicitHeight: 1
color: Appearance.m3colors.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4
}
StateLayer {
Layout.fillWidth: true
contentItem: RowLayout {
spacing: 8
anchors.fill: parent
anchors.margins: 12
MaterialIcon { text: Config.options.background.showDesktopIcons ? "visibility_off" : "visibility"; font.pointSize: 20 }
CustomText { text: Config.options.background.showDesktopIcons ? "Hide icons" : "Show icons"; Layout.fillWidth: true }
}
onClicked: {
Config.options.background.showDesktopIcons = !Config.options.background.showDesktopIcons
root.close()
}
}
}
}
function openAt(mouseX, mouseY, parentW, parentH) {
menuX = Math.min(mouseX, parentW - popupBackground.implicitWidth)
menuY = Math.min(mouseY, parentH - popupBackground.implicitHeight)
visible = true
}
function close() {
visible = false
}
}
@@ -0,0 +1,193 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import qs.Components
import qs.Config
Item {
id: contextMenu
anchors.fill: parent
z: 999
visible: false
property string targetFilePath: ""
property bool targetIsDir: false
property var targetAppEntry: null
property var targetPaths: []
signal openFileRequested(string path, bool isDir)
signal renameRequested(string path)
property real menuX: 0
property real menuY: 0
MouseArea {
anchors.fill: parent
onClicked: contextMenu.close()
}
CustomClippingRect {
id: popupBackground
readonly property real padding: 4
x: contextMenu.menuX
y: contextMenu.menuY
color: DynamicColors.tPalette.m3surface
radius: Appearance.rounding.normal
implicitWidth: menuLayout.implicitWidth + padding * 2
implicitHeight: menuLayout.implicitHeight + padding * 2
Behavior on opacity { Anim {} }
opacity: contextMenu.visible ? 1 : 0
ColumnLayout {
id: menuLayout
anchors.fill: parent
anchors.margins: popupBackground.padding
spacing: 0
StateLayer {
Layout.fillWidth: true
contentItem: RowLayout {
spacing: 8
anchors.fill: parent
anchors.margins: 12
MaterialIcon { text: "open_in_new"; font.pointSize: 20 }
CustomText { text: "Open"; Layout.fillWidth: true }
}
onClicked: {
for (let i = 0; i < contextMenu.targetPaths.length; i++) {
let p = contextMenu.targetPaths[i];
if (p === contextMenu.targetFilePath) {
if (p.endsWith(".desktop") && contextMenu.targetAppEntry) contextMenu.targetAppEntry.execute()
else contextMenu.openFileRequested(p, contextMenu.targetIsDir)
} else {
Quickshell.execDetached(["xdg-open", p])
}
}
contextMenu.close()
}
}
StateLayer {
Layout.fillWidth: true
contentItem: RowLayout {
spacing: 8
anchors.fill: parent
anchors.margins: 12
MaterialIcon { text: contextMenu.targetIsDir ? "terminal" : "apps"; font.pointSize: 20 }
CustomText { text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."; Layout.fillWidth: true }
}
onClicked: {
Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath])
contextMenu.close()
}
}
CustomRect {
Layout.fillWidth: true
implicitHeight: 1
color: DynamicColors.palette.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4
}
StateLayer {
Layout.fillWidth: true
contentItem: RowLayout {
spacing: 8
anchors.fill: parent
anchors.margins: 12
MaterialIcon { text: "content_copy"; font.pointSize: 20 }
CustomText { text: "Copy path"; Layout.fillWidth: true }
}
onClicked: {
Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")])
contextMenu.close()
}
}
StateLayer {
Layout.fillWidth: true
visible: contextMenu.targetPaths.length === 1
contentItem: RowLayout {
spacing: 8
anchors.fill: parent
anchors.margins: 12
MaterialIcon { text: "edit"; font.pointSize: 20 }
CustomText { text: "Rename"; Layout.fillWidth: true }
}
onClicked: {
contextMenu.renameRequested(contextMenu.targetFilePath)
contextMenu.close()
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 1
color: Appearance.m3colors.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4
}
StateLayer {
id: deleteButton
Layout.fillWidth: true
colBackgroundHover: Appearance.colors.colError
contentItem: RowLayout {
spacing: 8
anchors.fill: parent
anchors.margins: 12
MaterialIcon {
text: "delete";
font.pointSize: 20;
color: deleteButton.hovered ? Appearance.colors.colOnError : Appearance.colors.colError
}
CustomText {
text: "Move to trash";
Layout.fillWidth: true;
color: deleteButton.hovered ? Appearance.colors.colOnError : Appearance.colors.colError
}
}
onClicked: {
let cmd = ["gio", "trash"].concat(contextMenu.targetPaths)
Quickshell.execDetached(cmd)
contextMenu.close()
}
}
}
}
function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) {
targetFilePath = path
targetIsDir = isDir
targetAppEntry = appEnt
targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path]
menuX = Math.min(mouseX, parentW - popupBackground.implicitWidth)
menuY = Math.min(mouseY, parentH - popupBackground.implicitHeight)
visible = true
}
function close() {
visible = false
}
}
@@ -0,0 +1,273 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.Config
import qs.Components
import qs.Helpers
Item {
id: delegateRoot
property var appEntry: fileName.endsWith(".desktop") ? DesktopEntries.byId(DesktopUtils.getAppId(fileName)) : null
property bool fileIsDir: model.isDir
property string fileName: model.fileName
property string filePath: model.filePath
property int gridX: model.gridX
property int gridY: model.gridY
property bool isSnapping: snapAnimX.running || snapAnimY.running
property string resolvedIcon: {
if (fileName.endsWith(".desktop")) {
if (appEntry && appEntry.icon && appEntry.icon !== "")
return appEntry.icon;
return AppSearch.guessIcon(DesktopUtils.getAppId(fileName));
} else if (DesktopUtils.getFileType(fileName, fileIsDir) === "image") {
return "file://" + filePath;
} else {
return DesktopUtils.getIconName(fileName, fileIsDir);
}
}
function compensateAndSnap(absVisX, absVisY) {
dragContainer.x = absVisX - delegateRoot.x;
dragContainer.y = absVisY - delegateRoot.y;
snapAnimX.start();
snapAnimY.start();
}
function getDragX() {
return dragContainer.x;
}
function getDragY() {
return dragContainer.y;
}
height: root.cellHeight
width: root.cellWidth
x: gridX * root.cellWidth
y: gridY * root.cellHeight
Behavior on x {
enabled: !mouseArea.drag.active && !isSnapping && !root.selectedIcons.includes(filePath)
Anim {
}
}
Behavior on y {
enabled: !mouseArea.drag.active && !isSnapping && !root.selectedIcons.includes(filePath)
Anim {
}
}
Item {
id: dragContainer
height: parent.height
width: parent.width
states: State {
when: mouseArea.drag.active
PropertyChanges {
opacity: 0.8
scale: 1.1
target: dragContainer
z: 100
}
}
transform: Translate {
x: (root.selectedIcons.includes(filePath) && root.dragLeader !== "" && root.dragLeader !== filePath) ? root.groupDragX : 0
y: (root.selectedIcons.includes(filePath) && root.dragLeader !== "" && root.dragLeader !== filePath) ? root.groupDragY : 0
}
transitions: Transition {
Anim {
}
}
onXChanged: {
if (mouseArea.drag.active) {
root.dragLeader = filePath;
root.groupDragX = x;
}
}
onYChanged: {
if (mouseArea.drag.active) {
root.dragLeader = filePath;
root.groupDragY = y;
}
}
PropertyAnimation {
id: snapAnimX
duration: 250
easing.type: Easing.OutCubic
property: "x"
target: dragContainer
to: 0
}
PropertyAnimation {
id: snapAnimY
duration: 250
easing.type: Easing.OutCubic
property: "y"
target: dragContainer
to: 0
}
Column {
anchors.centerIn: parent
spacing: 6
IconImage {
anchors.horizontalCenter: parent.horizontalCenter
implicitSize: 48
source: {
if (delegateRoot.resolvedIcon.startsWith("file://") || delegateRoot.resolvedIcon.startsWith("/")) {
return delegateRoot.resolvedIcon;
} else {
return Quickshell.iconPath(delegateRoot.resolvedIcon, fileIsDir ? "folder" : "text-x-generic");
}
}
}
Item {
height: 40
width: 88
CustomText {
anchors.fill: parent
color: "white"
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
style: Text.Outline
styleColor: "black"
text: (appEntry && appEntry.name !== "") ? appEntry.name : fileName
visible: !renameLoader.active
wrapMode: Text.Wrap
}
Loader {
id: renameLoader
active: root.editingFilePath === filePath
anchors.centerIn: parent
height: 24
width: 110
sourceComponent: CustomTextInput {
anchors.fill: parent
anchors.margins: 2
color: "white"
horizontalAlignment: Text.AlignHCenter
text: fileName
wrapMode: Text.Wrap
Component.onCompleted: {
forceActiveFocus();
selectAll();
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (text.trim() !== "" && text !== fileName) {
let newName = text.trim();
let newPath = filePath.substring(0, filePath.lastIndexOf('/') + 1) + newName;
Quickshell.execDetached(["mv", filePath, newPath]);
}
root.editingFilePath = "";
event.accepted = true;
} else if (event.key === Qt.Key_Escape) {
root.editingFilePath = "";
event.accepted = true;
}
}
onActiveFocusChanged: {
if (!activeFocus && root.editingFilePath === filePath) {
root.editingFilePath = "";
}
}
}
}
}
}
CustomRect {
anchors.fill: parent
anchors.margins: 4
color: "white"
opacity: root.selectedIcons.includes(filePath) ? 0.2 : 0.0
radius: 8
Behavior on opacity {
Anim {
}
}
}
MouseArea {
id: mouseArea
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
drag.target: dragContainer
hoverEnabled: true
onClicked: mouse => {
root.forceActiveFocus();
if (mouse.button === Qt.RightButton) {
if (!root.selectedIcons.includes(filePath)) {
root.selectedIcons = [filePath];
}
let pos = mapToItem(root, mouse.x, mouse.y);
root.contextMenu.openAt(pos.x, pos.y, filePath, fileIsDir, appEntry, root.width, root.height, root.selectedIcons);
} else {
root.selectedIcons = [filePath];
root.contextMenu.close();
}
}
onDoubleClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (filePath.endsWith(".desktop") && appEntry)
appEntry.execute();
else
root.exec(filePath, fileIsDir);
}
}
onPressed: mouse => {
if (mouse.button === Qt.LeftButton && !root.selectedIcons.includes(filePath)) {
root.selectedIcons = [filePath];
}
}
onReleased: {
if (drag.active) {
let absoluteX = delegateRoot.x + dragContainer.x;
let absoluteY = delegateRoot.y + dragContainer.y;
let snapX = Math.max(0, Math.round(absoluteX / root.cellWidth));
let snapY = Math.max(0, Math.round(absoluteY / root.cellHeight));
root.performMassDrop(filePath, snapX, snapY);
}
}
CustomRect {
anchors.fill: parent
anchors.margins: 4
color: "white"
opacity: parent.containsMouse ? 0.1 : 0.0
radius: 8
Behavior on opacity {
Anim {
}
}
}
}
}
}
+162
View File
@@ -0,0 +1,162 @@
import QtQuick
import Quickshell
import qs.Modules
import qs.Helpers
import qs.Paths
import ZShell.Services
Item {
id: root
property int cellHeight: 110
property int cellWidth: 100
property var contextMenu: desktopMenu
property string dragLeader: ""
property string editingFilePath: ""
property real groupDragX: 0
property real groupDragY: 0
property var selectedIcons: []
property real startX: 0
property real startY: 0
function exec(filePath, isDir) {
const cmd = ["xdg-open", filePath];
Quickshell.execDetached(cmd);
}
function performMassDrop(leaderPath, targetX, targetY) {
let maxCol = Math.max(0, Math.floor(gridArea.width / cellWidth) - 1);
let maxRow = Math.max(0, Math.floor(gridArea.height / cellHeight) - 1);
let visuals = [];
for (let i = 0; i < gridArea.children.length; i++) {
let child = gridArea.children[i];
if (child.filePath && root.selectedIcons.includes(child.filePath)) {
let isLeader = (root.dragLeader === child.filePath);
let offsetX = isLeader ? child.getDragX() : root.groupDragX;
let offsetY = isLeader ? child.getDragY() : root.groupDragY;
visuals.push({
childRef: child,
absX: child.x + offsetX,
absY: child.y + offsetY
});
}
}
desktopModel.massMove(root.selectedIcons, leaderPath, targetX, targetY, maxCol, maxRow);
for (let i = 0; i < visuals.length; i++) {
visuals[i].childRef.compensateAndSnap(visuals[i].absX, visuals[i].absY);
}
root.dragLeader = "";
root.groupDragX = 0;
root.groupDragY = 0;
}
anchors.fill: parent
focus: true
Keys.onPressed: event => {
if (event.key === Qt.Key_F2 && selectedIcons.length > 0)
editingFilePath = selectedIcons[0];
}
DesktopModel {
id: desktopModel
Component.onCompleted: loadDirectory(FileUtils.trimFileProtocol(Paths.desktop))
}
Rectangle {
id: lasso
border.color: Appearance.colors.colPrimary
border.width: 1
color: DynamicColors.tPalette.m3primary
radius: Appearance.rounding.small
visible: false
z: 99
}
MouseArea {
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
onPositionChanged: mouse => {
if (lasso.visible) {
lasso.x = Math.min(mouse.x, root.startX);
lasso.y = Math.min(mouse.y, root.startY);
lasso.width = Math.abs(mouse.x - root.startX);
lasso.height = Math.abs(mouse.y - root.startY);
let minCol = Math.floor((lasso.x - gridArea.x) / cellWidth);
let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / cellWidth);
let minRow = Math.floor((lasso.y - gridArea.y) / cellHeight);
let maxRow = Math.floor((lasso.y + lasso.height - gridArea.y) / cellHeight);
let newSelection = [];
for (let i = 0; i < gridArea.children.length; i++) {
let child = gridArea.children[i];
if (child.filePath !== undefined && child.gridX >= minCol && child.gridX <= maxCol && child.gridY >= minRow && child.gridY <= maxRow) {
newSelection.push(child.filePath);
}
}
root.selectedIcons = newSelection;
}
}
onPressed: mouse => {
root.editingFilePath = "";
desktopMenu.close();
if (mouse.button === Qt.RightButton) {
root.selectedIcons = [];
bgContextMenu.openAt(mouse.x, mouse.y, root.width, root.height);
} else {
bgContextMenu.close();
root.selectedIcons = [];
root.startX = mouse.x;
root.startY = mouse.y;
lasso.x = mouse.x;
lasso.y = mouse.y;
lasso.width = 0;
lasso.height = 0;
lasso.visible = true;
}
}
onReleased: {
lasso.visible = false;
}
}
Item {
id: gridArea
anchors.fill: parent
anchors.margins: 20
anchors.topMargin: 40
visible: true
Repeater {
model: desktopModel
delegate: DesktopIconDelegate {
property int itemIndex: index
}
}
}
DesktopIconContextMenu {
id: desktopMenu
onOpenFileRequested: (path, isDir) => root.exec(path, isDir)
onRenameRequested: path => {
root.editingFilePath = path;
}
}
BackgroundContextMenu {
id: bgContextMenu
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ CustomRect {
MaterialIcon {
animate: true
color: Players.active?.isPlaying ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
font.pointSize: Appearance.font.size.normal
font.pointSize: Appearance.font.size.larger
text: Players.active?.isPlaying ? "music_note" : "music_off"
}
+18 -62
View File
@@ -19,91 +19,47 @@ RowLayout {
property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 80
clip: true
percentage: 0
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
onPercentageChanged: {
const next = percentage;
// Canvas {
// id: gaugeCanvas
//
// anchors.centerIn: parent
// height: width
// width: Math.min(parent.width, parent.height)
//
// Component.onCompleted: requestPaint()
// onPaint: {
// const ctx = getContext("2d");
// ctx.reset();
// const cx = width / 2;
// const cy = (height / 2) + 1;
// const radius = (Math.min(width, height) - 12) / 2;
// const lineWidth = 3;
// ctx.beginPath();
// ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep);
// ctx.lineWidth = lineWidth;
// ctx.lineCap = "round";
// ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2);
// ctx.stroke();
// if (root.animatedPercentage > 0) {
// ctx.beginPath();
// ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage);
// ctx.lineWidth = lineWidth;
// ctx.lineCap = "round";
// ctx.strokeStyle = root.accentColor;
// ctx.stroke();
// }
// }
//
// Connections {
// function onAnimatedPercentageChanged() {
// gaugeCanvas.requestPaint();
// }
//
// target: root
// }
//
// Connections {
// function onPaletteChanged() {
// gaugeCanvas.requestPaint();
// }
//
// target: DynamicColors
// }
// }
if (Math.abs(next - animatedPercentage) >= 0.05)
animatedPercentage = next;
}
MaterialIcon {
id: icon
color: DynamicColors.palette.m3onSurface
font.pointSize: 12
font.pointSize: Appearance.font.size.larger
text: root.icon
}
CustomClippingRect {
Layout.preferredHeight: root.height
Layout.preferredWidth: 5
Layout.preferredHeight: root.height - Appearance.padding.small
Layout.preferredWidth: 4
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
radius: Appearance.rounding.full
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: root.mainColor
implicitHeight: root.percentage * parent.height
radius: implicitHeight / 2
id: fill
Behavior on implicitHeight {
Anim {
}
anchors.fill: parent
antialiasing: false
color: root.mainColor
implicitHeight: Math.ceil(root.percentage * parent.height)
radius: Appearance.rounding.full
transform: Scale {
origin.y: fill.height
yScale: Math.max(0.001, root.animatedPercentage)
}
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ CustomRect {
spacing: Appearance.spacing.small
MaterialIcon {
font.pointSize: Appearance.font.size.normal
font.pointSize: Appearance.font.size.larger
text: "package_2"
}
+4
View File
@@ -2,6 +2,7 @@ import Quickshell
import QtQuick
import Quickshell.Wayland
import qs.Config
import qs.Modules.DesktopIcons
Loader {
active: Config.background.enabled
@@ -30,6 +31,9 @@ Loader {
WallBackground {
}
DesktopIcons {
}
}
}
}
+1
View File
@@ -10,6 +10,7 @@ Singleton {
readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell`
readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell`
readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell`
readonly property string desktop: `${Quickshell.env("XDG_DATA_HOME") || `${home}/Desktop`}`
readonly property string home: Quickshell.env("HOME")
readonly property string imagecache: `${cache}/imagecache`
readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell"
+1
View File
@@ -4,6 +4,7 @@ pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0)
if(NOT Cava_FOUND)
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
endif()
+4
View File
@@ -7,7 +7,11 @@ qml_module(ZShell-services
audiocollector.hpp audiocollector.cpp
audioprovider.hpp audioprovider.cpp
cavaprovider.hpp cavaprovider.cpp
desktopmodel.hpp desktopmodel.cpp
desktopstatemanager.hpp desktopstatemanager.cpp
LIBRARIES
Qt6::Core
Qt6::Qml
PkgConfig::Pipewire
PkgConfig::Aubio
PkgConfig::Cava
+186
View File
@@ -0,0 +1,186 @@
#include "desktopmodel.hpp"
#include "desktopstatemanager.hpp"
#include <QDir>
#include <QFileInfoList>
namespace ZShell::services {
DesktopModel::DesktopModel(QObject *parent) : QAbstractListModel(parent) {
}
int DesktopModel::rowCount(const QModelIndex &parent) const {
if (parent.isValid()) return 0;
return m_items.count();
}
QVariant DesktopModel::data(const QModelIndex &index, int role) const {
if (!index.isValid() || index.row() >= m_items.size()) return QVariant();
const DesktopItem &item = m_items[index.row()];
switch (role) {
case FileNameRole: return item.fileName;
case FilePathRole: return item.filePath;
case IsDirRole: return item.isDir;
case GridXRole: return item.gridX;
case GridYRole: return item.gridY;
default: return QVariant();
}
}
QHash<int, QByteArray> DesktopModel::roleNames() const {
QHash<int, QByteArray> roles;
roles[FileNameRole] = "fileName";
roles[FilePathRole] = "filePath";
roles[IsDirRole] = "isDir";
roles[GridXRole] = "gridX";
roles[GridYRole] = "gridY";
return roles;
}
void DesktopModel::loadDirectory(const QString &path) {
beginResetModel();
m_items.clear();
QDir dir(path);
dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
QFileInfoList list = dir.entryInfoList();
DesktopStateManager sm;
QVariantMap savedLayout = sm.getLayout();
for (const QFileInfo &fileInfo : list) {
DesktopItem item;
item.fileName = fileInfo.fileName();
item.filePath = fileInfo.absoluteFilePath();
item.isDir = fileInfo.isDir();
if (savedLayout.contains(item.fileName)) {
QVariantMap pos = savedLayout[item.fileName].toMap();
item.gridX = pos["x"].toInt();
item.gridY = pos["y"].toInt();
} else {
// TODO: make getEmptySpot in C++ and call it here to get the initial position for new icons
item.gridX = 0;
item.gridY = 0;
}
m_items.append(item);
}
endResetModel();
}
void DesktopModel::moveIcon(int index, int newX, int newY) {
if (index < 0 || index >= m_items.size()) return;
m_items[index].gridX = newX;
m_items[index].gridY = newY;
QModelIndex modelIndex = createIndex(index, 0);
emit dataChanged(modelIndex, modelIndex, {GridXRole, GridYRole});
saveCurrentLayout();
}
void DesktopModel::saveCurrentLayout() {
QVariantMap layout;
for (const auto& item : m_items) {
QVariantMap pos;
pos["x"] = item.gridX;
pos["y"] = item.gridY;
layout[item.fileName] = pos;
}
DesktopStateManager sm;
sm.saveLayout(layout);
}
void DesktopModel::massMove(const QVariantList& selectedPathsList, const QString& leaderPath, int targetX, int targetY, int maxCol, int maxRow) {
QStringList selectedPaths;
for (const QVariant& v : selectedPathsList) {
selectedPaths << v.toString();
}
if (selectedPaths.isEmpty()) return;
int oldX = 0, oldY = 0;
for (const auto& item : m_items) {
if (item.filePath == leaderPath) {
oldX = item.gridX;
oldY = item.gridY;
break;
}
}
int deltaX = targetX - oldX;
int deltaY = targetY - oldY;
if (deltaX == 0 && deltaY == 0) return;
if (selectedPaths.size() == 1 && targetX >= 0 && targetX <= maxCol && targetY >= 0 && targetY <= maxRow) {
QString movingPath = selectedPaths.first();
int movingIndex = -1;
int targetIndex = -1;
for (int i = 0; i < m_items.size(); ++i) {
if (m_items[i].filePath == movingPath) {
movingIndex = i;
} else if (m_items[i].gridX == targetX && m_items[i].gridY == targetY) {
targetIndex = i;
}
}
if (targetIndex != -1 && movingIndex != -1) {
m_items[targetIndex].gridX = oldX;
m_items[targetIndex].gridY = oldY;
m_items[movingIndex].gridX = targetX;
m_items[movingIndex].gridY = targetY;
emit dataChanged(index(0, 0), index(m_items.size() - 1, 0), {GridXRole, GridYRole});
saveCurrentLayout();
return;
}
}
QList<DesktopItem*> movingItems;
QSet<QString> occupied;
for (int i = 0; i < m_items.size(); ++i) {
if (selectedPaths.contains(m_items[i].filePath)) {
movingItems.append(&m_items[i]);
} else {
occupied.insert(QString::number(m_items[i].gridX) + "," + QString::number(m_items[i].gridY));
}
}
for (auto* item : movingItems) {
int newX = item->gridX + deltaX;
int newY = item->gridY + deltaY;
bool outOfBounds = newX < 0 || newX > maxCol || newY < 0 || newY > maxRow;
bool collision = occupied.contains(QString::number(newX) + "," + QString::number(newY));
if (outOfBounds || collision) {
bool found = false;
for (int x = 0; x <= maxCol && !found; ++x) {
for (int y = 0; y <= maxRow && !found; ++y) {
QString key = QString::number(x) + "," + QString::number(y);
if (!occupied.contains(key)) {
newX = x;
newY = y;
occupied.insert(key);
found = true;
}
}
}
} else {
occupied.insert(QString::number(newX) + "," + QString::number(newY));
}
item->gridX = newX;
item->gridY = newY;
}
emit dataChanged(index(0, 0), index(m_items.size() - 1, 0), {GridXRole, GridYRole});
saveCurrentLayout();
}
} // namespace ZShell::services
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <QAbstractListModel>
#include <QList>
#include <QString>
#include <QQmlEngine>
namespace ZShell::services {
struct DesktopItem {
QString fileName;
QString filePath;
bool isDir;
int gridX;
int gridY;
};
class DesktopModel : public QAbstractListModel {
Q_OBJECT
QML_ELEMENT
public:
enum DesktopRoles {
FileNameRole = Qt::UserRole + 1,
FilePathRole,
IsDirRole,
GridXRole,
GridYRole
};
explicit DesktopModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void loadDirectory(const QString &path);
Q_INVOKABLE void moveIcon(int index, int newX, int newY);
Q_INVOKABLE void massMove(const QVariantList &selectedPathsList, const QString &leaderPath, int targetX, int targetY, int maxCol, int maxRow);
private:
QList<DesktopItem> m_items;
void saveCurrentLayout();
};
} // namespace ZShell::services
@@ -0,0 +1,54 @@
#include "desktopstatemanager.hpp"
#include <QStandardPaths>
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>
namespace ZShell::services {
DesktopStateManager::DesktopStateManager(QObject *parent) : QObject(parent) {
}
QString DesktopStateManager::getConfigFilePath() const {
QString configDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/sleex";
QDir dir(configDir);
if (!dir.exists()) {
dir.mkpath(".");
}
return configDir + "/desktop_layout.json";
}
void DesktopStateManager::saveLayout(const QVariantMap& layout) {
QJsonObject jsonObj = QJsonObject::fromVariantMap(layout);
QJsonDocument doc(jsonObj);
QFile file(getConfigFilePath());
if (file.open(QIODevice::WriteOnly)) {
file.write(doc.toJson(QJsonDocument::Indented));
file.close();
} else {
qWarning() << "Sleex: Impossible de sauvegarder le layout du bureau dans" << getConfigFilePath();
}
}
QVariantMap DesktopStateManager::getLayout() {
QFile file(getConfigFilePath());
if (!file.open(QIODevice::ReadOnly)) {
return QVariantMap();
}
QByteArray data = file.readAll();
file.close();
QJsonDocument doc = QJsonDocument::fromJson(data);
if (doc.isObject()) {
return doc.object().toVariantMap();
}
return QVariantMap();
}
} // namespace ZShell::services
@@ -0,0 +1,24 @@
#pragma once
#include <QObject>
#include <QVariantMap>
#include <QQmlEngine>
namespace ZShell::services {
class DesktopStateManager : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
explicit DesktopStateManager(QObject *parent = nullptr);
Q_INVOKABLE void saveLayout(const QVariantMap& layout);
Q_INVOKABLE QVariantMap getLayout();
private:
QString getConfigFilePath() const;
};
} // namespace ZShell::services