diff --git a/Components/Menu.qml b/Components/Menu.qml index 70e6cd8..bf19a84 100644 --- a/Components/Menu.qml +++ b/Components/Menu.qml @@ -30,6 +30,7 @@ MouseArea { signal itemSelected(item: MenuItem) anchors.fill: parent + cursorShape: undefined enabled: expanded layer.enabled: opacity < 1 opacity: expanded ? 1 : 0 diff --git a/Drawers/Windows.qml b/Drawers/Windows.qml index 933216f..af41dcd 100644 --- a/Drawers/Windows.qml +++ b/Drawers/Windows.qml @@ -26,25 +26,30 @@ CustomWindow { WlrLayershell.exclusionMode: ExclusionMode.Ignore // WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None color: "transparent" - contentItem.focus: true mask: visibilities.isDrawing ? null : region name: "Bar" contentItem.Keys.onEscapePressed: { if (Config.barConfig.autoHide) visibilities.bar = false; + visibilities.launcher = false; visibilities.sidebar = false; visibilities.dashboard = false; visibilities.osd = false; visibilities.settings = false; visibilities.resources = false; + visibilities.dock = false; + panels.popouts.hasCurrent = false; } onHasFullscreenChanged: { visibilities.launcher = false; + visibilities.sidebar = false; visibilities.dashboard = false; visibilities.osd = false; visibilities.settings = false; visibilities.resources = false; + visibilities.dock = false; + panels.popouts.hasCurrent = false; } Region { @@ -229,7 +234,7 @@ CustomWindow { PanelBg { id: utilsBg - deformAmount: panels.sidebar.visible ? (0.1) : (0.1) + deformAmount: 0.1 exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] panel: panels.utilities topLeftRadius: 0 @@ -238,9 +243,9 @@ CustomWindow { PanelBg { id: popoutBg - property real extraHeight: panels.popouts.isDetached ? 0 : 0.2 + property real extraHeight: 0.2 - deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1 + deformAmount: panels.popouts.currentName.startsWith("traymenu") ? 0.15 : 0.08 implicitHeight: panels.popouts.height * (1 + extraHeight) implicitWidth: panels.popouts.width panel: panels.popoutsWrapper diff --git a/Helpers/AppSearch.qml b/Helpers/AppSearch.qml index 400b3d9..feb863d 100644 --- a/Helpers/AppSearch.qml +++ b/Helpers/AppSearch.qml @@ -1,19 +1,13 @@ pragma Singleton import Quickshell -import Quickshell.Io -import "../scripts/levendist.js" as Levendist +import qs.Helpers import "../scripts/fuzzysort.js" as Fuzzy -import qs.Config Singleton { id: root - readonly property list list: Array.from(DesktopEntries.applications.values).filter((app, index, self) => index === self.findIndex(t => (t.id === app.id))) - readonly property var preppedIcons: list.map(a => ({ - name: Fuzzy.prepare(`${a.icon} `), - entry: a - })) + readonly property list list: Array.from(DesktopEntries.applications.values) readonly property var preppedNames: list.map(a => ({ name: Fuzzy.prepare(`${a.name} `), entry: a @@ -36,8 +30,7 @@ Singleton { "replace": "system-lock-screen" } ] - readonly property real scoreGapThreshold: 0.1 - readonly property real scoreThreshold: 0.6 + property real scoreThreshold: 0.2 property var substitutions: ({ "code-url-handler": "visual-studio-code", "Code": "visual-studio-code", @@ -45,64 +38,27 @@ Singleton { "pavucontrol-qt": "pavucontrol", "wps": "wps-office2019-kprometheus", "wpsoffice": "wps-office2019-kprometheus", - "footclient": "foot" + "footclient": "foot", + "zen": "zen-browser" }) - function bestFuzzyEntry(search: string, preppedList: list, key: string): var { - const results = Fuzzy.go(search, preppedList, { - key: key, - threshold: root.scoreThreshold, - limit: 2 + signal reload + + function fuzzyQuery(search: string): var { + return Fuzzy.go(search, preppedNames, { + all: true, + key: "name" + }).map(r => { + return r.obj.entry; }); - - if (!results || results.length === 0) - return null; - - const best = results[0]; - const second = results.length > 1 ? results[1] : null; - - if (second && (best.score - second.score) < root.scoreGapThreshold) - return null; - - return best.obj.entry; - } - - function fuzzyQuery(search: string, preppedList: list): var { - const entry = bestFuzzyEntry(search, preppedList, "name"); - return entry ? [entry] : []; - } - - function getKebabNormalizedAppName(str: string): string { - return str.toLowerCase().replace(/\s+/g, "-"); - } - - function getReverseDomainNameAppName(str: string): string { - return str.split('.').slice(-1)[0]; - } - - function getUndescoreToKebabAppName(str: string): string { - return str.toLowerCase().replace(/_/g, "-"); } function guessIcon(str) { if (!str || str.length == 0) return "image-missing"; - if (iconExists(str)) - return str; - - const entry = DesktopEntries.byId(str); - if (entry) - return entry.icon; - - const heuristicEntry = DesktopEntries.heuristicLookup(str); - if (heuristicEntry) - return heuristicEntry.icon; - if (substitutions[str]) return substitutions[str]; - if (substitutions[str.toLowerCase()]) - return substitutions[str.toLowerCase()]; for (let i = 0; i < regexSubstitutions.length; i++) { const substitution = regexSubstitutions[i]; @@ -111,35 +67,25 @@ Singleton { return replacedName; } - const lowercased = str.toLowerCase(); - if (iconExists(lowercased)) - return lowercased; + if (iconExists(str)) + return str; - const reverseDomainNameAppName = getReverseDomainNameAppName(str); - if (iconExists(reverseDomainNameAppName)) - return reverseDomainNameAppName; + let guessStr = str; + guessStr = str.split('.').slice(-1)[0].toLowerCase(); + if (iconExists(guessStr)) + return guessStr; + guessStr = str.toLowerCase().replace(/\s+/g, "-"); + if (iconExists(guessStr)) + return guessStr; + const searchResults = root.fuzzyQuery(str); + if (searchResults.length > 0) { + const firstEntry = searchResults[0]; + guessStr = firstEntry.icon; + if (iconExists(guessStr)) + return guessStr; + } - const lowercasedDomainNameAppName = reverseDomainNameAppName.toLowerCase(); - if (iconExists(lowercasedDomainNameAppName)) - return lowercasedDomainNameAppName; - - const kebabNormalizedGuess = getKebabNormalizedAppName(str); - if (iconExists(kebabNormalizedGuess)) - return kebabNormalizedGuess; - - const undescoreToKebabGuess = getUndescoreToKebabAppName(str); - if (iconExists(undescoreToKebabGuess)) - return undescoreToKebabGuess; - - const iconSearchResult = fuzzyQuery(str, preppedIcons); - if (iconSearchResult && iconExists(iconSearchResult.icon)) - return iconSearchResult.icon; - - const nameSearchResult = root.fuzzyQuery(str, preppedNames); - if (nameSearchResult && iconExists(nameSearchResult.icon)) - return nameSearchResult.icon; - - return "application-x-executable"; + return str; } function iconExists(iconName) { diff --git a/Modules/DesktopIcons/DesktopIconContextMenu.qml b/Modules/DesktopIcons/DesktopIconContextMenu.qml index e32cf9e..78d5c8c 100644 --- a/Modules/DesktopIcons/DesktopIconContextMenu.qml +++ b/Modules/DesktopIcons/DesktopIconContextMenu.qml @@ -6,227 +6,264 @@ import qs.Components import qs.Config Item { - id: contextMenu + id: contextMenu - anchors.fill: parent - z: 999 - visible: false + property real menuX: 0 + property real menuY: 0 + property var targetAppEntry: null + property string targetFilePath: "" + property bool targetIsDir: false + property var targetPaths: [] - property string targetFilePath: "" - property bool targetIsDir: false - property var targetAppEntry: null + signal openFileRequested(string path, bool isDir) + signal renameRequested(string path) - property var targetPaths: [] + function close() { + visible = false; + } - signal openFileRequested(string path, bool isDir) - signal renameRequested(string path) + function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) { + targetFilePath = path; + targetIsDir = isDir; + targetAppEntry = appEnt; - property real menuX: 0 - property real menuY: 0 + targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path]; - CustomClippingRect { - id: popupBackground - readonly property real padding: Appearance.padding.small + menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth)); + menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight)); - x: contextMenu.menuX - y: contextMenu.menuY + visible = true; + } - color: DynamicColors.tPalette.m3surface - radius: Appearance.rounding.normal + anchors.fill: parent + visible: false + z: 999 - implicitWidth: menuLayout.implicitWidth + padding * 2 - implicitHeight: menuLayout.implicitHeight + padding * 2 + CustomClippingRect { + id: popupBackground - Behavior on opacity { Anim {} } - opacity: contextMenu.visible ? 1 : 0 + readonly property real padding: Appearance.padding.small - ColumnLayout { - id: menuLayout - anchors.centerIn: parent - spacing: 0 + color: DynamicColors.tPalette.m3surface + implicitHeight: menuLayout.implicitHeight + padding * 2 + implicitWidth: menuLayout.implicitWidth + padding * 2 + opacity: contextMenu.visible ? 1 : 0 + radius: Appearance.rounding.normal + x: contextMenu.menuX + y: contextMenu.menuY - CustomRect { - Layout.preferredWidth: 160 - radius: popupBackground.radius - popupBackground.padding - implicitHeight: openRow.implicitHeight + Appearance.padding.small * 2 + Behavior on opacity { + Anim { + } + } - RowLayout { - id: openRow - spacing: 8 - anchors.fill: parent - anchors.leftMargin: Appearance.padding.smaller + ColumnLayout { + id: menuLayout - MaterialIcon { text: "open_in_new"; font.pointSize: 20 } - CustomText { text: "Open"; Layout.fillWidth: true } - } + anchors.centerIn: parent + spacing: 0 - StateLayer { - anchors.fill: parent + CustomRect { + Layout.preferredWidth: 160 + implicitHeight: openRow.implicitHeight + Appearance.padding.small * 2 + radius: popupBackground.radius - popupBackground.padding - 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() - } - } - } + RowLayout { + id: openRow - CustomRect { - Layout.fillWidth: true - radius: popupBackground.radius - popupBackground.padding - implicitHeight: openWithRow.implicitHeight + Appearance.padding.small * 2 + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + spacing: 8 - RowLayout { - id: openWithRow - spacing: 8 - anchors.fill: parent - anchors.leftMargin: Appearance.padding.smaller + MaterialIcon { + font.pointSize: 20 + text: "open_in_new" + } - MaterialIcon { text: contextMenu.targetIsDir ? "terminal" : "apps"; font.pointSize: 20 } - CustomText { text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."; Layout.fillWidth: true } - } + CustomText { + Layout.fillWidth: true + text: "Open" + } + } - StateLayer { - anchors.fill: parent + StateLayer { + anchors.fill: parent + + 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(); + } + } + } + + CustomRect { + Layout.fillWidth: true + implicitHeight: openWithRow.implicitHeight + Appearance.padding.small * 2 + radius: popupBackground.radius - popupBackground.padding + + RowLayout { + id: openWithRow + + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + spacing: 8 + + MaterialIcon { + font.pointSize: 20 + text: contextMenu.targetIsDir ? "terminal" : "apps" + } + + CustomText { + Layout.fillWidth: true + text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..." + } + } + + StateLayer { + anchors.fill: parent onClicked: { if (contextMenu.targetIsDir) { - Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", contextMenu.targetFilePath]) + Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", contextMenu.targetFilePath]); } else { - Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath]) + Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath]); } - contextMenu.close() + contextMenu.close(); } - } - } + } + } - CustomRect { - Layout.fillWidth: true - implicitHeight: 1 - color: DynamicColors.palette.m3outlineVariant - Layout.topMargin: 4 - Layout.bottomMargin: 4 - } + CustomRect { + Layout.bottomMargin: 4 + Layout.fillWidth: true + Layout.topMargin: 4 + color: DynamicColors.palette.m3outlineVariant + implicitHeight: 1 + } - CustomRect { - Layout.fillWidth: true - radius: popupBackground.radius - popupBackground.padding - implicitHeight: copyPathRow.implicitHeight + Appearance.padding.small * 2 + CustomRect { + Layout.fillWidth: true + implicitHeight: copyPathRow.implicitHeight + Appearance.padding.small * 2 + radius: popupBackground.radius - popupBackground.padding - RowLayout { - id: copyPathRow - spacing: 8 - anchors.fill: parent - anchors.leftMargin: Appearance.padding.smaller + RowLayout { + id: copyPathRow - MaterialIcon { text: "content_copy"; font.pointSize: 20 } - CustomText { text: "Copy path"; Layout.fillWidth: true } - } + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + spacing: 8 - StateLayer { - anchors.fill: parent + MaterialIcon { + font.pointSize: 20 + text: "content_copy" + } - onClicked: { - Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]) - contextMenu.close() - } - } - } + CustomText { + Layout.fillWidth: true + text: "Copy path" + } + } - CustomRect { - Layout.fillWidth: true - visible: contextMenu.targetPaths.length === 1 - radius: popupBackground.radius - popupBackground.padding - implicitHeight: renameRow.implicitHeight + Appearance.padding.small * 2 + StateLayer { + anchors.fill: parent - RowLayout { - id: renameRow - spacing: 8 - anchors.fill: parent - anchors.leftMargin: Appearance.padding.smaller + onClicked: { + Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]); + contextMenu.close(); + } + } + } - MaterialIcon { text: "edit"; font.pointSize: 20 } - CustomText { text: "Rename"; Layout.fillWidth: true } - } + CustomRect { + Layout.fillWidth: true + implicitHeight: renameRow.implicitHeight + Appearance.padding.small * 2 + radius: popupBackground.radius - popupBackground.padding + visible: contextMenu.targetPaths.length === 1 - StateLayer { - anchors.fill: parent + RowLayout { + id: renameRow - onClicked: { - contextMenu.renameRequested(contextMenu.targetFilePath) - contextMenu.close() - } - } - } + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + spacing: 8 - Rectangle { - Layout.fillWidth: true - implicitHeight: 1 - color: DynamicColors.palette.m3outlineVariant - Layout.topMargin: 4 - Layout.bottomMargin: 4 - } + MaterialIcon { + font.pointSize: 20 + text: "edit" + } - CustomRect { - Layout.fillWidth: true - radius: popupBackground.radius - popupBackground.padding - implicitHeight: deleteRow.implicitHeight + Appearance.padding.small * 2 + CustomText { + Layout.fillWidth: true + text: "Rename" + } + } - RowLayout { - id: deleteRow - spacing: 8 - anchors.fill: parent - anchors.leftMargin: Appearance.padding.smaller + StateLayer { + anchors.fill: parent - MaterialIcon { - text: "delete" - font.pointSize: 20 - color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error - } + onClicked: { + contextMenu.renameRequested(contextMenu.targetFilePath); + contextMenu.close(); + } + } + } - CustomText { - text: "Move to trash" - Layout.fillWidth: true - color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error - } - } + Rectangle { + Layout.bottomMargin: 4 + Layout.fillWidth: true + Layout.topMargin: 4 + color: DynamicColors.palette.m3outlineVariant + implicitHeight: 1 + } - StateLayer { - id: deleteButton - anchors.fill: parent - color: DynamicColors.tPalette.m3error + CustomRect { + Layout.fillWidth: true + implicitHeight: deleteRow.implicitHeight + Appearance.padding.small * 2 + radius: popupBackground.radius - popupBackground.padding - onClicked: { - let cmd = ["gio", "trash"].concat(contextMenu.targetPaths) - Quickshell.execDetached(cmd) - contextMenu.close() - } - } - } - } - } + RowLayout { + id: deleteRow - function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) { - targetFilePath = path - targetIsDir = isDir - targetAppEntry = appEnt + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + spacing: 8 - targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path] + MaterialIcon { + color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error + font.pointSize: 20 + text: "delete" + } - menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth)) - menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight)) + CustomText { + Layout.fillWidth: true + color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error + text: "Move to trash" + } + } - visible = true - } + StateLayer { + id: deleteButton - function close() { - visible = false - } + anchors.fill: parent + color: DynamicColors.tPalette.m3error + + onClicked: { + let cmd = ["gio", "trash"].concat(contextMenu.targetPaths); + Quickshell.execDetached(cmd); + contextMenu.close(); + } + } + } + } + } } diff --git a/Modules/DesktopIcons/DesktopIconDelegate.qml b/Modules/DesktopIcons/DesktopIconDelegate.qml index 674a005..4a9d51a 100644 --- a/Modules/DesktopIcons/DesktopIconDelegate.qml +++ b/Modules/DesktopIcons/DesktopIconDelegate.qml @@ -6,16 +6,19 @@ import qs.Components import qs.Helpers Item { - id: delegateRoot + id: root 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 + required property var contextMenu + property bool fileIsDir: modelData.isDir + property string fileName: modelData.fileName + property string filePath: modelData.filePath + property int gridX: modelData.gridX + property int gridY: modelData.gridY + required property Item iconsRoot property bool isSnapping: snapAnimX.running || snapAnimY.running property bool lassoActive + required property var modelData property string resolvedIcon: { if (fileName.endsWith(".desktop")) { if (appEntry && appEntry.icon && appEntry.icon !== "") @@ -29,8 +32,8 @@ Item { } function compensateAndSnap(absVisX, absVisY) { - dragContainer.x = absVisX - delegateRoot.x; - dragContainer.y = absVisY - delegateRoot.y; + dragContainer.x = absVisX - root.x; + dragContainer.y = absVisY - root.y; snapAnimX.start(); snapAnimY.start(); } @@ -43,19 +46,19 @@ Item { return dragContainer.y; } - height: root.cellHeight - width: root.cellWidth - x: gridX * root.cellWidth - y: gridY * root.cellHeight + height: root.iconsRoot.cellHeight + width: root.iconsRoot.cellWidth + x: gridX * root.iconsRoot.cellWidth + y: gridY * root.iconsRoot.cellHeight Behavior on x { - enabled: !mouseArea.drag.active && !isSnapping && !root.selectedIcons.includes(filePath) + enabled: !mouseArea.drag.active && !root.isSnapping && !root.iconsRoot.selectedIcons.includes(root.filePath) Anim { } } Behavior on y { - enabled: !mouseArea.drag.active && !isSnapping && !root.selectedIcons.includes(filePath) + enabled: !mouseArea.drag.active && !root.isSnapping && !root.iconsRoot.selectedIcons.includes(root.filePath) Anim { } @@ -78,8 +81,8 @@ Item { } } 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 + x: (root.iconsRoot.selectedIcons.includes(root.filePath) && root.iconsRoot.dragLeader !== "" && root.iconsRoot.dragLeader !== root.filePath) ? root.iconsRoot.groupDragX : 0 + y: (root.iconsRoot.selectedIcons.includes(root.filePath) && root.iconsRoot.dragLeader !== "" && root.iconsRoot.dragLeader !== root.filePath) ? root.iconsRoot.groupDragY : 0 } transitions: Transition { Anim { @@ -88,14 +91,14 @@ Item { onXChanged: { if (mouseArea.drag.active) { - root.dragLeader = filePath; - root.groupDragX = x; + root.iconsRoot.dragLeader = root.filePath; + root.iconsRoot.groupDragX = x; } } onYChanged: { if (mouseArea.drag.active) { - root.dragLeader = filePath; - root.groupDragY = y; + root.iconsRoot.dragLeader = root.filePath; + root.iconsRoot.groupDragY = y; } } @@ -127,10 +130,10 @@ Item { anchors.horizontalCenter: parent.horizontalCenter implicitSize: 48 source: { - if (delegateRoot.resolvedIcon.startsWith("file://") || delegateRoot.resolvedIcon.startsWith("/")) { - return delegateRoot.resolvedIcon; + if (root.resolvedIcon.startsWith("file://") || root.resolvedIcon.startsWith("/")) { + return root.resolvedIcon; } else { - return Quickshell.iconPath(delegateRoot.resolvedIcon, fileIsDir ? "folder" : "text-x-generic"); + return Quickshell.iconPath(root.resolvedIcon, root.fileIsDir ? "folder" : "text-x-generic"); } } } @@ -147,7 +150,7 @@ Item { maximumLineCount: 2 style: Text.Outline styleColor: "black" - text: (appEntry && appEntry.name !== "") ? appEntry.name : fileName + text: (root.appEntry && root.appEntry.name !== "") ? root.appEntry.name : root.fileName visible: !renameLoader.active wrapMode: Text.Wrap } @@ -155,7 +158,7 @@ Item { Loader { id: renameLoader - active: root.editingFilePath === filePath + active: root.iconsRoot.editingFilePath === root.filePath anchors.centerIn: parent height: 24 width: 110 @@ -165,7 +168,7 @@ Item { anchors.margins: 2 color: "white" horizontalAlignment: Text.AlignHCenter - text: fileName + text: root.fileName wrapMode: Text.Wrap Component.onCompleted: { @@ -174,22 +177,22 @@ Item { } Keys.onPressed: function (event) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - if (text.trim() !== "" && text !== fileName) { + if (text.trim() !== "" && text !== root.fileName) { let newName = text.trim(); - let newPath = filePath.substring(0, filePath.lastIndexOf('/') + 1) + newName; + let newPath = root.filePath.substring(0, root.filePath.lastIndexOf('/') + 1) + newName; - Quickshell.execDetached(["mv", filePath, newPath]); + Quickshell.execDetached(["mv", root.filePath, newPath]); } - root.editingFilePath = ""; + root.iconsRoot.editingFilePath = ""; event.accepted = true; } else if (event.key === Qt.Key_Escape) { - root.editingFilePath = ""; + root.iconsRoot.editingFilePath = ""; event.accepted = true; } } onActiveFocusChanged: { - if (!activeFocus && root.editingFilePath === filePath) { - root.editingFilePath = ""; + if (!activeFocus && root.iconsRoot.editingFilePath === root.filePath) { + root.iconsRoot.editingFilePath = ""; } } } @@ -201,7 +204,7 @@ Item { anchors.fill: parent anchors.margins: 4 color: "white" - opacity: root.selectedIcons.includes(filePath) ? 0.2 : 0.0 + opacity: root.iconsRoot.selectedIcons.includes(root.filePath) ? 0.2 : 0.0 radius: Appearance.rounding.smallest Behavior on opacity { @@ -215,45 +218,45 @@ Item { acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: parent - cursorShape: root.lassoActive ? undefined : Qt.PointingHandCursor + cursorShape: root.iconsRoot.lassoActive ? undefined : Qt.PointingHandCursor drag.target: dragContainer hoverEnabled: true onClicked: mouse => { - root.forceActiveFocus(); + root.iconsRoot.forceActiveFocus(); if (mouse.button === Qt.RightButton) { - if (!root.selectedIcons.includes(filePath)) { - root.selectedIcons = [filePath]; + if (!root.iconsRoot.selectedIcons.includes(root.filePath)) { + root.iconsRoot.selectedIcons = [root.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); + let pos = mapToItem(root.iconsRoot, mouse.x, mouse.y); + root.contextMenu.openAt(pos.x, pos.y, root.filePath, root.fileIsDir, root.appEntry, root.iconsRoot.width, root.iconsRoot.height, root.iconsRoot.selectedIcons); } else { - root.selectedIcons = [filePath]; + root.iconsRoot.selectedIcons = [root.filePath]; root.contextMenu.close(); } } onDoubleClicked: mouse => { if (mouse.button === Qt.LeftButton) { - if (filePath.endsWith(".desktop") && appEntry) - appEntry.execute(); + if (root.filePath.endsWith(".desktop") && root.appEntry) + root.appEntry.execute(); else - root.exec(filePath, fileIsDir); + root.iconsRoot.exec(root.filePath, root.fileIsDir); } } onPressed: mouse => { - if (mouse.button === Qt.LeftButton && !root.selectedIcons.includes(filePath)) { - root.selectedIcons = [filePath]; + if (mouse.button === Qt.LeftButton && !root.iconsRoot.selectedIcons.includes(root.filePath)) { + root.iconsRoot.selectedIcons = [root.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)); + let absoluteX = root.x + dragContainer.x; + let absoluteY = root.y + dragContainer.y; + let snapX = Math.max(0, Math.round(absoluteX / root.iconsRoot.cellWidth)); + let snapY = Math.max(0, Math.round(absoluteY / root.iconsRoot.cellHeight)); - root.performMassDrop(filePath, snapX, snapY); + root.iconsRoot.performMassDrop(root.filePath, snapX, snapY); } } diff --git a/Modules/DesktopIcons/DesktopIcons.qml b/Modules/DesktopIcons/DesktopIcons.qml index d5272a3..0264e81 100644 --- a/Modules/DesktopIcons/DesktopIcons.qml +++ b/Modules/DesktopIcons/DesktopIcons.qml @@ -1,11 +1,12 @@ +pragma ComponentBehavior: Bound + import QtQuick import Quickshell -import qs.Modules +import ZShell.Services import qs.Helpers import qs.Config import qs.Components import qs.Paths -import ZShell.Services Item { id: root @@ -23,7 +24,34 @@ Item { property real startY: 0 function exec(filePath, isDir) { - const cmd = ["xdg-open", filePath]; + let type = DesktopUtils.getFileType(filePath, isDir); + let cmd = []; + switch (type) { + case "image": + cmd = [Config.options.apps.imageViewer, filePath]; + break; + case "video": + cmd = [Config.options.apps.videoPlayer, filePath]; + break; + case "audio": + cmd = [Config.options.apps.audioPlayer, filePath]; + break; + case "archive": + cmd = [Config.options.apps.archiveManager, filePath]; + break; + case "directory": + cmd = [Config.options.apps.fileManager, filePath]; + break; + case "code": + case "text": + cmd = [Config.options.apps.textEditor, filePath]; + break; + case "document": + cmd = [Config.options.apps.documentViewer, filePath]; + break; + default: + cmd = ["xdg-open", filePath]; + } Quickshell.execDetached(cmd); } @@ -57,6 +85,7 @@ Item { root.groupDragY = 0; } + anchors.fill: parent focus: true Keys.onPressed: event => { @@ -67,6 +96,8 @@ Item { DesktopModel { id: desktopModel + rows: Math.max(1, Math.floor(gridArea.height / root.cellHeight)) + Component.onCompleted: loadDirectory(FileUtils.trimFileProtocol(Paths.desktop)) } @@ -133,10 +164,10 @@ Item { 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 minCol = Math.floor((lasso.x - gridArea.x) / root.cellWidth); + let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / root.cellWidth); + let minRow = Math.floor((lasso.y - gridArea.y) / root.cellHeight); + let maxRow = Math.floor((lasso.y + lasso.height - gridArea.y) / root.cellHeight); let newSelection = []; for (let i = 0; i < gridArea.children.length; i++) { @@ -158,10 +189,10 @@ Item { } else { bgContextMenu.close(); root.selectedIcons = []; - root.startX = Math.floor(mouse.x); - root.startY = Math.floor(mouse.y); - lasso.x = Math.floor(mouse.x); - lasso.y = Math.floor(mouse.y); + root.startX = mouse.x; + root.startY = mouse.y; + lasso.x = mouse.x; + lasso.y = mouse.y; lasso.width = 0; lasso.height = 0; lasso.showLasso(); @@ -178,15 +209,15 @@ Item { anchors.fill: parent anchors.margins: 20 anchors.topMargin: 40 - visible: true Repeater { model: desktopModel delegate: DesktopIconDelegate { - property int itemIndex: index + required property int index - lassoActive: root.lassoActive + contextMenu: desktopMenu + iconsRoot: root } } } @@ -202,6 +233,5 @@ Item { BackgroundContextMenu { id: bgContextMenu - } } diff --git a/Plugins/ZShell/Services/desktopmodel.cpp b/Plugins/ZShell/Services/desktopmodel.cpp index a75cd98..2fa55c7 100644 --- a/Plugins/ZShell/Services/desktopmodel.cpp +++ b/Plugins/ZShell/Services/desktopmodel.cpp @@ -2,6 +2,7 @@ #include "desktopstatemanager.hpp" #include #include +#include namespace ZShell::services { @@ -37,7 +38,29 @@ QHash DesktopModel::roleNames() const { return roles; } +QPoint DesktopModel::getEmptySpot(const QSet &occupied) const { + for (int x = 0; ; ++x) { + for (int y = 0; y < m_rows; ++y) { + QString key = QString::number(x) + "," + QString::number(y); + if (!occupied.contains(key)) { + return QPoint(x, y); + } + } + } +} + void DesktopModel::loadDirectory(const QString &path) { + m_watchedPath = path; + + if (!m_watcher.directories().isEmpty()) + m_watcher.removePaths(m_watcher.directories()); + + m_watcher.addPath(path); + + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, + this, &DesktopModel::onDirectoryChanged, + Qt::UniqueConnection); + beginResetModel(); m_items.clear(); @@ -48,6 +71,14 @@ void DesktopModel::loadDirectory(const QString &path) { DesktopStateManager sm; QVariantMap savedLayout = sm.getLayout(); + QSet occupied; + for (const QFileInfo &fileInfo : list) { + if (savedLayout.contains(fileInfo.fileName())) { + QVariantMap pos = savedLayout[fileInfo.fileName()].toMap(); + occupied.insert(QString::number(pos["x"].toInt()) + "," + QString::number(pos["y"].toInt())); + } + } + for (const QFileInfo &fileInfo : list) { DesktopItem item; item.fileName = fileInfo.fileName(); @@ -59,15 +90,20 @@ void DesktopModel::loadDirectory(const QString &path) { 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; + QPoint spot = getEmptySpot(occupied); + item.gridX = spot.x(); + item.gridY = spot.y(); + occupied.insert(QString::number(item.gridX) + "," + QString::number(item.gridY)); } m_items.append(item); } endResetModel(); } +void DesktopModel::onDirectoryChanged() { + loadDirectory(m_watchedPath); +} + void DesktopModel::moveIcon(int index, int newX, int newY) { if (index < 0 || index >= m_items.size()) return; @@ -183,4 +219,4 @@ void DesktopModel::massMove(const QVariantList& selectedPathsList, const QString saveCurrentLayout(); } -} // namespace ZShell::services +}; diff --git a/Plugins/ZShell/Services/desktopmodel.hpp b/Plugins/ZShell/Services/desktopmodel.hpp index dcaa363..286b40a 100644 --- a/Plugins/ZShell/Services/desktopmodel.hpp +++ b/Plugins/ZShell/Services/desktopmodel.hpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include namespace ZShell::services { @@ -39,9 +39,27 @@ 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); +Q_PROPERTY(int rows READ rows WRITE setRows NOTIFY rowsChanged) + +public: +[[nodiscard]] int rows() const { + return m_rows; +} +void setRows(int r) { + if (m_rows != r) { m_rows = r; emit rowsChanged(); } +} + +signals: +void rowsChanged(); + private: +int m_rows = 1; QList m_items; +QString m_watchedPath; +QFileSystemWatcher m_watcher; void saveCurrentLayout(); +[[nodiscard]] QPoint getEmptySpot(const QSet &occupied) const; +void onDirectoryChanged(); }; -} // namespace ZShell::Services +}; diff --git a/Plugins/ZShell/Services/desktopstatemanager.cpp b/Plugins/ZShell/Services/desktopstatemanager.cpp index 55f9c70..820a1e6 100644 --- a/Plugins/ZShell/Services/desktopstatemanager.cpp +++ b/Plugins/ZShell/Services/desktopstatemanager.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace ZShell::services { @@ -12,7 +13,7 @@ DesktopStateManager::DesktopStateManager(QObject *parent) : QObject(parent) { } QString DesktopStateManager::getConfigFilePath() const { - QString configDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/sleex"; + QString configDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/zshell"; QDir dir(configDir); if (!dir.exists()) { dir.mkpath("."); @@ -29,7 +30,7 @@ void DesktopStateManager::saveLayout(const QVariantMap& layout) { file.write(doc.toJson(QJsonDocument::Indented)); file.close(); } else { - qWarning() << "Sleex: Impossible de sauvegarder le layout du bureau dans" << getConfigFilePath(); + qWarning() << "zshell: Cannot save desktop layout to" << getConfigFilePath(); } }