Merge settings window to main #23
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user