more efficient desktop icons
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 9s
Python / lint-format (pull_request) Successful in 16s
Python / test (pull_request) Successful in 30s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m6s

This commit is contained in:
2026-06-10 14:40:39 +02:00
parent 01556e66f3
commit 6b77ebd9be
9 changed files with 413 additions and 336 deletions
+1
View File
@@ -30,6 +30,7 @@ MouseArea {
signal itemSelected(item: MenuItem) signal itemSelected(item: MenuItem)
anchors.fill: parent anchors.fill: parent
cursorShape: undefined
enabled: expanded enabled: expanded
layer.enabled: opacity < 1 layer.enabled: opacity < 1
opacity: expanded ? 1 : 0 opacity: expanded ? 1 : 0
+9 -4
View File
@@ -26,25 +26,30 @@ CustomWindow {
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
// WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None // WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
color: "transparent" color: "transparent"
contentItem.focus: true
mask: visibilities.isDrawing ? null : region mask: visibilities.isDrawing ? null : region
name: "Bar" name: "Bar"
contentItem.Keys.onEscapePressed: { contentItem.Keys.onEscapePressed: {
if (Config.barConfig.autoHide) if (Config.barConfig.autoHide)
visibilities.bar = false; visibilities.bar = false;
visibilities.launcher = false;
visibilities.sidebar = false; visibilities.sidebar = false;
visibilities.dashboard = false; visibilities.dashboard = false;
visibilities.osd = false; visibilities.osd = false;
visibilities.settings = false; visibilities.settings = false;
visibilities.resources = false; visibilities.resources = false;
visibilities.dock = false;
panels.popouts.hasCurrent = false;
} }
onHasFullscreenChanged: { onHasFullscreenChanged: {
visibilities.launcher = false; visibilities.launcher = false;
visibilities.sidebar = false;
visibilities.dashboard = false; visibilities.dashboard = false;
visibilities.osd = false; visibilities.osd = false;
visibilities.settings = false; visibilities.settings = false;
visibilities.resources = false; visibilities.resources = false;
visibilities.dock = false;
panels.popouts.hasCurrent = false;
} }
Region { Region {
@@ -229,7 +234,7 @@ CustomWindow {
PanelBg { PanelBg {
id: utilsBg id: utilsBg
deformAmount: panels.sidebar.visible ? (0.1) : (0.1) deformAmount: 0.1
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg]
panel: panels.utilities panel: panels.utilities
topLeftRadius: 0 topLeftRadius: 0
@@ -238,9 +243,9 @@ CustomWindow {
PanelBg { PanelBg {
id: popoutBg 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) implicitHeight: panels.popouts.height * (1 + extraHeight)
implicitWidth: panels.popouts.width implicitWidth: panels.popouts.width
panel: panels.popoutsWrapper panel: panels.popoutsWrapper
+30 -84
View File
@@ -1,19 +1,13 @@
pragma Singleton pragma Singleton
import Quickshell import Quickshell
import Quickshell.Io import qs.Helpers
import "../scripts/levendist.js" as Levendist
import "../scripts/fuzzysort.js" as Fuzzy import "../scripts/fuzzysort.js" as Fuzzy
import qs.Config
Singleton { Singleton {
id: root id: root
readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values).filter((app, index, self) => index === self.findIndex(t => (t.id === app.id))) readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values)
readonly property var preppedIcons: list.map(a => ({
name: Fuzzy.prepare(`${a.icon} `),
entry: a
}))
readonly property var preppedNames: list.map(a => ({ readonly property var preppedNames: list.map(a => ({
name: Fuzzy.prepare(`${a.name} `), name: Fuzzy.prepare(`${a.name} `),
entry: a entry: a
@@ -36,8 +30,7 @@ Singleton {
"replace": "system-lock-screen" "replace": "system-lock-screen"
} }
] ]
readonly property real scoreGapThreshold: 0.1 property real scoreThreshold: 0.2
readonly property real scoreThreshold: 0.6
property var substitutions: ({ property var substitutions: ({
"code-url-handler": "visual-studio-code", "code-url-handler": "visual-studio-code",
"Code": "visual-studio-code", "Code": "visual-studio-code",
@@ -45,64 +38,27 @@ Singleton {
"pavucontrol-qt": "pavucontrol", "pavucontrol-qt": "pavucontrol",
"wps": "wps-office2019-kprometheus", "wps": "wps-office2019-kprometheus",
"wpsoffice": "wps-office2019-kprometheus", "wpsoffice": "wps-office2019-kprometheus",
"footclient": "foot" "footclient": "foot",
"zen": "zen-browser"
}) })
function bestFuzzyEntry(search: string, preppedList: list<var>, key: string): var { signal reload
const results = Fuzzy.go(search, preppedList, {
key: key, function fuzzyQuery(search: string): var {
threshold: root.scoreThreshold, return Fuzzy.go(search, preppedNames, {
limit: 2 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>): 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) { function guessIcon(str) {
if (!str || str.length == 0) if (!str || str.length == 0)
return "image-missing"; 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]) if (substitutions[str])
return substitutions[str]; return substitutions[str];
if (substitutions[str.toLowerCase()])
return substitutions[str.toLowerCase()];
for (let i = 0; i < regexSubstitutions.length; i++) { for (let i = 0; i < regexSubstitutions.length; i++) {
const substitution = regexSubstitutions[i]; const substitution = regexSubstitutions[i];
@@ -111,35 +67,25 @@ Singleton {
return replacedName; return replacedName;
} }
const lowercased = str.toLowerCase(); if (iconExists(str))
if (iconExists(lowercased)) return str;
return lowercased;
const reverseDomainNameAppName = getReverseDomainNameAppName(str); let guessStr = str;
if (iconExists(reverseDomainNameAppName)) guessStr = str.split('.').slice(-1)[0].toLowerCase();
return reverseDomainNameAppName; 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(); return str;
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";
} }
function iconExists(iconName) { function iconExists(iconName) {
+114 -77
View File
@@ -8,56 +8,82 @@ import qs.Config
Item { Item {
id: contextMenu id: contextMenu
anchors.fill: parent property real menuX: 0
z: 999 property real menuY: 0
visible: false property var targetAppEntry: null
property string targetFilePath: "" property string targetFilePath: ""
property bool targetIsDir: false property bool targetIsDir: false
property var targetAppEntry: null
property var targetPaths: [] property var targetPaths: []
signal openFileRequested(string path, bool isDir) signal openFileRequested(string path, bool isDir)
signal renameRequested(string path) signal renameRequested(string path)
property real menuX: 0 function close() {
property real menuY: 0 visible = false;
}
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.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth));
menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight));
visible = true;
}
anchors.fill: parent
visible: false
z: 999
CustomClippingRect { CustomClippingRect {
id: popupBackground id: popupBackground
readonly property real padding: Appearance.padding.small readonly property real padding: Appearance.padding.small
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 x: contextMenu.menuX
y: contextMenu.menuY y: contextMenu.menuY
color: DynamicColors.tPalette.m3surface Behavior on opacity {
radius: Appearance.rounding.normal Anim {
}
implicitWidth: menuLayout.implicitWidth + padding * 2 }
implicitHeight: menuLayout.implicitHeight + padding * 2
Behavior on opacity { Anim {} }
opacity: contextMenu.visible ? 1 : 0
ColumnLayout { ColumnLayout {
id: menuLayout id: menuLayout
anchors.centerIn: parent anchors.centerIn: parent
spacing: 0 spacing: 0
CustomRect { CustomRect {
Layout.preferredWidth: 160 Layout.preferredWidth: 160
radius: popupBackground.radius - popupBackground.padding
implicitHeight: openRow.implicitHeight + Appearance.padding.small * 2 implicitHeight: openRow.implicitHeight + Appearance.padding.small * 2
radius: popupBackground.radius - popupBackground.padding
RowLayout { RowLayout {
id: openRow id: openRow
spacing: 8
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller anchors.leftMargin: Appearance.padding.smaller
spacing: 8
MaterialIcon { text: "open_in_new"; font.pointSize: 20 } MaterialIcon {
CustomText { text: "Open"; Layout.fillWidth: true } font.pointSize: 20
text: "open_in_new"
}
CustomText {
Layout.fillWidth: true
text: "Open"
}
} }
StateLayer { StateLayer {
@@ -67,30 +93,40 @@ Item {
for (let i = 0; i < contextMenu.targetPaths.length; i++) { for (let i = 0; i < contextMenu.targetPaths.length; i++) {
let p = contextMenu.targetPaths[i]; let p = contextMenu.targetPaths[i];
if (p === contextMenu.targetFilePath) { if (p === contextMenu.targetFilePath) {
if (p.endsWith(".desktop") && contextMenu.targetAppEntry) contextMenu.targetAppEntry.execute() if (p.endsWith(".desktop") && contextMenu.targetAppEntry)
else contextMenu.openFileRequested(p, contextMenu.targetIsDir) contextMenu.targetAppEntry.execute();
else
contextMenu.openFileRequested(p, contextMenu.targetIsDir);
} else { } else {
Quickshell.execDetached(["xdg-open", p]) Quickshell.execDetached(["xdg-open", p]);
} }
} }
contextMenu.close() contextMenu.close();
} }
} }
} }
CustomRect { CustomRect {
Layout.fillWidth: true Layout.fillWidth: true
radius: popupBackground.radius - popupBackground.padding
implicitHeight: openWithRow.implicitHeight + Appearance.padding.small * 2 implicitHeight: openWithRow.implicitHeight + Appearance.padding.small * 2
radius: popupBackground.radius - popupBackground.padding
RowLayout { RowLayout {
id: openWithRow id: openWithRow
spacing: 8
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller anchors.leftMargin: Appearance.padding.smaller
spacing: 8
MaterialIcon { text: contextMenu.targetIsDir ? "terminal" : "apps"; font.pointSize: 20 } MaterialIcon {
CustomText { text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."; Layout.fillWidth: true } font.pointSize: 20
text: contextMenu.targetIsDir ? "terminal" : "apps"
}
CustomText {
Layout.fillWidth: true
text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."
}
} }
StateLayer { StateLayer {
@@ -98,135 +134,136 @@ Item {
onClicked: { onClicked: {
if (contextMenu.targetIsDir) { if (contextMenu.targetIsDir) {
Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", contextMenu.targetFilePath]) Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", contextMenu.targetFilePath]);
} else { } else {
Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath]) Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath]);
} }
contextMenu.close() contextMenu.close();
} }
} }
} }
CustomRect { CustomRect {
Layout.fillWidth: true
implicitHeight: 1
color: DynamicColors.palette.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4 Layout.bottomMargin: 4
Layout.fillWidth: true
Layout.topMargin: 4
color: DynamicColors.palette.m3outlineVariant
implicitHeight: 1
} }
CustomRect { CustomRect {
Layout.fillWidth: true Layout.fillWidth: true
radius: popupBackground.radius - popupBackground.padding
implicitHeight: copyPathRow.implicitHeight + Appearance.padding.small * 2 implicitHeight: copyPathRow.implicitHeight + Appearance.padding.small * 2
radius: popupBackground.radius - popupBackground.padding
RowLayout { RowLayout {
id: copyPathRow id: copyPathRow
spacing: 8
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller anchors.leftMargin: Appearance.padding.smaller
spacing: 8
MaterialIcon { text: "content_copy"; font.pointSize: 20 } MaterialIcon {
CustomText { text: "Copy path"; Layout.fillWidth: true } font.pointSize: 20
text: "content_copy"
}
CustomText {
Layout.fillWidth: true
text: "Copy path"
}
} }
StateLayer { StateLayer {
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]) Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]);
contextMenu.close() contextMenu.close();
} }
} }
} }
CustomRect { CustomRect {
Layout.fillWidth: true Layout.fillWidth: true
visible: contextMenu.targetPaths.length === 1
radius: popupBackground.radius - popupBackground.padding
implicitHeight: renameRow.implicitHeight + Appearance.padding.small * 2 implicitHeight: renameRow.implicitHeight + Appearance.padding.small * 2
radius: popupBackground.radius - popupBackground.padding
visible: contextMenu.targetPaths.length === 1
RowLayout { RowLayout {
id: renameRow id: renameRow
spacing: 8
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller anchors.leftMargin: Appearance.padding.smaller
spacing: 8
MaterialIcon { text: "edit"; font.pointSize: 20 } MaterialIcon {
CustomText { text: "Rename"; Layout.fillWidth: true } font.pointSize: 20
text: "edit"
}
CustomText {
Layout.fillWidth: true
text: "Rename"
}
} }
StateLayer { StateLayer {
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
contextMenu.renameRequested(contextMenu.targetFilePath) contextMenu.renameRequested(contextMenu.targetFilePath);
contextMenu.close() contextMenu.close();
} }
} }
} }
Rectangle { Rectangle {
Layout.fillWidth: true
implicitHeight: 1
color: DynamicColors.palette.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4 Layout.bottomMargin: 4
Layout.fillWidth: true
Layout.topMargin: 4
color: DynamicColors.palette.m3outlineVariant
implicitHeight: 1
} }
CustomRect { CustomRect {
Layout.fillWidth: true Layout.fillWidth: true
radius: popupBackground.radius - popupBackground.padding
implicitHeight: deleteRow.implicitHeight + Appearance.padding.small * 2 implicitHeight: deleteRow.implicitHeight + Appearance.padding.small * 2
radius: popupBackground.radius - popupBackground.padding
RowLayout { RowLayout {
id: deleteRow id: deleteRow
spacing: 8
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller anchors.leftMargin: Appearance.padding.smaller
spacing: 8
MaterialIcon { MaterialIcon {
text: "delete"
font.pointSize: 20
color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error
font.pointSize: 20
text: "delete"
} }
CustomText { CustomText {
text: "Move to trash"
Layout.fillWidth: true Layout.fillWidth: true
color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error
text: "Move to trash"
} }
} }
StateLayer { StateLayer {
id: deleteButton id: deleteButton
anchors.fill: parent anchors.fill: parent
color: DynamicColors.tPalette.m3error color: DynamicColors.tPalette.m3error
onClicked: { onClicked: {
let cmd = ["gio", "trash"].concat(contextMenu.targetPaths) let cmd = ["gio", "trash"].concat(contextMenu.targetPaths);
Quickshell.execDetached(cmd) Quickshell.execDetached(cmd);
contextMenu.close() 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.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth))
menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight))
visible = true
}
function close() {
visible = false
}
} }
+54 -51
View File
@@ -6,16 +6,19 @@ import qs.Components
import qs.Helpers import qs.Helpers
Item { Item {
id: delegateRoot id: root
property var appEntry: fileName.endsWith(".desktop") ? DesktopEntries.byId(DesktopUtils.getAppId(fileName)) : null property var appEntry: fileName.endsWith(".desktop") ? DesktopEntries.byId(DesktopUtils.getAppId(fileName)) : null
property bool fileIsDir: model.isDir required property var contextMenu
property string fileName: model.fileName property bool fileIsDir: modelData.isDir
property string filePath: model.filePath property string fileName: modelData.fileName
property int gridX: model.gridX property string filePath: modelData.filePath
property int gridY: model.gridY property int gridX: modelData.gridX
property int gridY: modelData.gridY
required property Item iconsRoot
property bool isSnapping: snapAnimX.running || snapAnimY.running property bool isSnapping: snapAnimX.running || snapAnimY.running
property bool lassoActive property bool lassoActive
required property var modelData
property string resolvedIcon: { property string resolvedIcon: {
if (fileName.endsWith(".desktop")) { if (fileName.endsWith(".desktop")) {
if (appEntry && appEntry.icon && appEntry.icon !== "") if (appEntry && appEntry.icon && appEntry.icon !== "")
@@ -29,8 +32,8 @@ Item {
} }
function compensateAndSnap(absVisX, absVisY) { function compensateAndSnap(absVisX, absVisY) {
dragContainer.x = absVisX - delegateRoot.x; dragContainer.x = absVisX - root.x;
dragContainer.y = absVisY - delegateRoot.y; dragContainer.y = absVisY - root.y;
snapAnimX.start(); snapAnimX.start();
snapAnimY.start(); snapAnimY.start();
} }
@@ -43,19 +46,19 @@ Item {
return dragContainer.y; return dragContainer.y;
} }
height: root.cellHeight height: root.iconsRoot.cellHeight
width: root.cellWidth width: root.iconsRoot.cellWidth
x: gridX * root.cellWidth x: gridX * root.iconsRoot.cellWidth
y: gridY * root.cellHeight y: gridY * root.iconsRoot.cellHeight
Behavior on x { 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 { Anim {
} }
} }
Behavior on y { 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 { Anim {
} }
@@ -78,8 +81,8 @@ Item {
} }
} }
transform: Translate { transform: Translate {
x: (root.selectedIcons.includes(filePath) && root.dragLeader !== "" && root.dragLeader !== filePath) ? root.groupDragX : 0 x: (root.iconsRoot.selectedIcons.includes(root.filePath) && root.iconsRoot.dragLeader !== "" && root.iconsRoot.dragLeader !== root.filePath) ? root.iconsRoot.groupDragX : 0
y: (root.selectedIcons.includes(filePath) && root.dragLeader !== "" && root.dragLeader !== filePath) ? root.groupDragY : 0 y: (root.iconsRoot.selectedIcons.includes(root.filePath) && root.iconsRoot.dragLeader !== "" && root.iconsRoot.dragLeader !== root.filePath) ? root.iconsRoot.groupDragY : 0
} }
transitions: Transition { transitions: Transition {
Anim { Anim {
@@ -88,14 +91,14 @@ Item {
onXChanged: { onXChanged: {
if (mouseArea.drag.active) { if (mouseArea.drag.active) {
root.dragLeader = filePath; root.iconsRoot.dragLeader = root.filePath;
root.groupDragX = x; root.iconsRoot.groupDragX = x;
} }
} }
onYChanged: { onYChanged: {
if (mouseArea.drag.active) { if (mouseArea.drag.active) {
root.dragLeader = filePath; root.iconsRoot.dragLeader = root.filePath;
root.groupDragY = y; root.iconsRoot.groupDragY = y;
} }
} }
@@ -127,10 +130,10 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
implicitSize: 48 implicitSize: 48
source: { source: {
if (delegateRoot.resolvedIcon.startsWith("file://") || delegateRoot.resolvedIcon.startsWith("/")) { if (root.resolvedIcon.startsWith("file://") || root.resolvedIcon.startsWith("/")) {
return delegateRoot.resolvedIcon; return root.resolvedIcon;
} else { } 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 maximumLineCount: 2
style: Text.Outline style: Text.Outline
styleColor: "black" styleColor: "black"
text: (appEntry && appEntry.name !== "") ? appEntry.name : fileName text: (root.appEntry && root.appEntry.name !== "") ? root.appEntry.name : root.fileName
visible: !renameLoader.active visible: !renameLoader.active
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
@@ -155,7 +158,7 @@ Item {
Loader { Loader {
id: renameLoader id: renameLoader
active: root.editingFilePath === filePath active: root.iconsRoot.editingFilePath === root.filePath
anchors.centerIn: parent anchors.centerIn: parent
height: 24 height: 24
width: 110 width: 110
@@ -165,7 +168,7 @@ Item {
anchors.margins: 2 anchors.margins: 2
color: "white" color: "white"
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: fileName text: root.fileName
wrapMode: Text.Wrap wrapMode: Text.Wrap
Component.onCompleted: { Component.onCompleted: {
@@ -174,22 +177,22 @@ Item {
} }
Keys.onPressed: function (event) { Keys.onPressed: function (event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { 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 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; event.accepted = true;
} else if (event.key === Qt.Key_Escape) { } else if (event.key === Qt.Key_Escape) {
root.editingFilePath = ""; root.iconsRoot.editingFilePath = "";
event.accepted = true; event.accepted = true;
} }
} }
onActiveFocusChanged: { onActiveFocusChanged: {
if (!activeFocus && root.editingFilePath === filePath) { if (!activeFocus && root.iconsRoot.editingFilePath === root.filePath) {
root.editingFilePath = ""; root.iconsRoot.editingFilePath = "";
} }
} }
} }
@@ -201,7 +204,7 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: 4 anchors.margins: 4
color: "white" 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 radius: Appearance.rounding.smallest
Behavior on opacity { Behavior on opacity {
@@ -215,45 +218,45 @@ Item {
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent anchors.fill: parent
cursorShape: root.lassoActive ? undefined : Qt.PointingHandCursor cursorShape: root.iconsRoot.lassoActive ? undefined : Qt.PointingHandCursor
drag.target: dragContainer drag.target: dragContainer
hoverEnabled: true hoverEnabled: true
onClicked: mouse => { onClicked: mouse => {
root.forceActiveFocus(); root.iconsRoot.forceActiveFocus();
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
if (!root.selectedIcons.includes(filePath)) { if (!root.iconsRoot.selectedIcons.includes(root.filePath)) {
root.selectedIcons = [filePath]; root.iconsRoot.selectedIcons = [root.filePath];
} }
let pos = mapToItem(root, mouse.x, mouse.y); let pos = mapToItem(root.iconsRoot, mouse.x, mouse.y);
root.contextMenu.openAt(pos.x, pos.y, filePath, fileIsDir, appEntry, root.width, root.height, root.selectedIcons); root.contextMenu.openAt(pos.x, pos.y, root.filePath, root.fileIsDir, root.appEntry, root.iconsRoot.width, root.iconsRoot.height, root.iconsRoot.selectedIcons);
} else { } else {
root.selectedIcons = [filePath]; root.iconsRoot.selectedIcons = [root.filePath];
root.contextMenu.close(); root.contextMenu.close();
} }
} }
onDoubleClicked: mouse => { onDoubleClicked: mouse => {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
if (filePath.endsWith(".desktop") && appEntry) if (root.filePath.endsWith(".desktop") && root.appEntry)
appEntry.execute(); root.appEntry.execute();
else else
root.exec(filePath, fileIsDir); root.iconsRoot.exec(root.filePath, root.fileIsDir);
} }
} }
onPressed: mouse => { onPressed: mouse => {
if (mouse.button === Qt.LeftButton && !root.selectedIcons.includes(filePath)) { if (mouse.button === Qt.LeftButton && !root.iconsRoot.selectedIcons.includes(root.filePath)) {
root.selectedIcons = [filePath]; root.iconsRoot.selectedIcons = [root.filePath];
} }
} }
onReleased: { onReleased: {
if (drag.active) { if (drag.active) {
let absoluteX = delegateRoot.x + dragContainer.x; let absoluteX = root.x + dragContainer.x;
let absoluteY = delegateRoot.y + dragContainer.y; let absoluteY = root.y + dragContainer.y;
let snapX = Math.max(0, Math.round(absoluteX / root.cellWidth)); let snapX = Math.max(0, Math.round(absoluteX / root.iconsRoot.cellWidth));
let snapY = Math.max(0, Math.round(absoluteY / root.cellHeight)); let snapY = Math.max(0, Math.round(absoluteY / root.iconsRoot.cellHeight));
root.performMassDrop(filePath, snapX, snapY); root.iconsRoot.performMassDrop(root.filePath, snapX, snapY);
} }
} }
+45 -15
View File
@@ -1,11 +1,12 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Modules import ZShell.Services
import qs.Helpers import qs.Helpers
import qs.Config import qs.Config
import qs.Components import qs.Components
import qs.Paths import qs.Paths
import ZShell.Services
Item { Item {
id: root id: root
@@ -23,7 +24,34 @@ Item {
property real startY: 0 property real startY: 0
function exec(filePath, isDir) { 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); Quickshell.execDetached(cmd);
} }
@@ -57,6 +85,7 @@ Item {
root.groupDragY = 0; root.groupDragY = 0;
} }
anchors.fill: parent
focus: true focus: true
Keys.onPressed: event => { Keys.onPressed: event => {
@@ -67,6 +96,8 @@ Item {
DesktopModel { DesktopModel {
id: desktopModel id: desktopModel
rows: Math.max(1, Math.floor(gridArea.height / root.cellHeight))
Component.onCompleted: loadDirectory(FileUtils.trimFileProtocol(Paths.desktop)) Component.onCompleted: loadDirectory(FileUtils.trimFileProtocol(Paths.desktop))
} }
@@ -133,10 +164,10 @@ Item {
lasso.width = Math.abs(mouse.x - root.startX); lasso.width = Math.abs(mouse.x - root.startX);
lasso.height = Math.abs(mouse.y - root.startY); lasso.height = Math.abs(mouse.y - root.startY);
let minCol = Math.floor((lasso.x - gridArea.x) / cellWidth); let minCol = Math.floor((lasso.x - gridArea.x) / root.cellWidth);
let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / cellWidth); let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / root.cellWidth);
let minRow = Math.floor((lasso.y - gridArea.y) / cellHeight); let minRow = Math.floor((lasso.y - gridArea.y) / root.cellHeight);
let maxRow = Math.floor((lasso.y + lasso.height - gridArea.y) / cellHeight); let maxRow = Math.floor((lasso.y + lasso.height - gridArea.y) / root.cellHeight);
let newSelection = []; let newSelection = [];
for (let i = 0; i < gridArea.children.length; i++) { for (let i = 0; i < gridArea.children.length; i++) {
@@ -158,10 +189,10 @@ Item {
} else { } else {
bgContextMenu.close(); bgContextMenu.close();
root.selectedIcons = []; root.selectedIcons = [];
root.startX = Math.floor(mouse.x); root.startX = mouse.x;
root.startY = Math.floor(mouse.y); root.startY = mouse.y;
lasso.x = Math.floor(mouse.x); lasso.x = mouse.x;
lasso.y = Math.floor(mouse.y); lasso.y = mouse.y;
lasso.width = 0; lasso.width = 0;
lasso.height = 0; lasso.height = 0;
lasso.showLasso(); lasso.showLasso();
@@ -178,15 +209,15 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: 20 anchors.margins: 20
anchors.topMargin: 40 anchors.topMargin: 40
visible: true
Repeater { Repeater {
model: desktopModel model: desktopModel
delegate: DesktopIconDelegate { delegate: DesktopIconDelegate {
property int itemIndex: index required property int index
lassoActive: root.lassoActive contextMenu: desktopMenu
iconsRoot: root
} }
} }
} }
@@ -202,6 +233,5 @@ Item {
BackgroundContextMenu { BackgroundContextMenu {
id: bgContextMenu id: bgContextMenu
} }
} }
+40 -4
View File
@@ -2,6 +2,7 @@
#include "desktopstatemanager.hpp" #include "desktopstatemanager.hpp"
#include <QDir> #include <QDir>
#include <QFileInfoList> #include <QFileInfoList>
#include <QPoint>
namespace ZShell::services { namespace ZShell::services {
@@ -37,7 +38,29 @@ QHash<int, QByteArray> DesktopModel::roleNames() const {
return roles; return roles;
} }
QPoint DesktopModel::getEmptySpot(const QSet<QString> &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) { 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(); beginResetModel();
m_items.clear(); m_items.clear();
@@ -48,6 +71,14 @@ void DesktopModel::loadDirectory(const QString &path) {
DesktopStateManager sm; DesktopStateManager sm;
QVariantMap savedLayout = sm.getLayout(); QVariantMap savedLayout = sm.getLayout();
QSet<QString> 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) { for (const QFileInfo &fileInfo : list) {
DesktopItem item; DesktopItem item;
item.fileName = fileInfo.fileName(); item.fileName = fileInfo.fileName();
@@ -59,15 +90,20 @@ void DesktopModel::loadDirectory(const QString &path) {
item.gridX = pos["x"].toInt(); item.gridX = pos["x"].toInt();
item.gridY = pos["y"].toInt(); item.gridY = pos["y"].toInt();
} else { } else {
// TODO: make getEmptySpot in C++ and call it here to get the initial position for new icons QPoint spot = getEmptySpot(occupied);
item.gridX = 0; item.gridX = spot.x();
item.gridY = 0; item.gridY = spot.y();
occupied.insert(QString::number(item.gridX) + "," + QString::number(item.gridY));
} }
m_items.append(item); m_items.append(item);
} }
endResetModel(); endResetModel();
} }
void DesktopModel::onDirectoryChanged() {
loadDirectory(m_watchedPath);
}
void DesktopModel::moveIcon(int index, int newX, int newY) { void DesktopModel::moveIcon(int index, int newX, int newY) {
if (index < 0 || index >= m_items.size()) return; if (index < 0 || index >= m_items.size()) return;
@@ -183,4 +219,4 @@ void DesktopModel::massMove(const QVariantList& selectedPathsList, const QString
saveCurrentLayout(); saveCurrentLayout();
} }
} // namespace ZShell::services };
+20 -2
View File
@@ -4,7 +4,7 @@
#include <QList> #include <QList>
#include <QString> #include <QString>
#include <QQmlEngine> #include <QQmlEngine>
#include <cstdint> #include <QFileSystemWatcher>
namespace ZShell::services { 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 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_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: private:
int m_rows = 1;
QList<DesktopItem> m_items; QList<DesktopItem> m_items;
QString m_watchedPath;
QFileSystemWatcher m_watcher;
void saveCurrentLayout(); void saveCurrentLayout();
[[nodiscard]] QPoint getEmptySpot(const QSet<QString> &occupied) const;
void onDirectoryChanged();
}; };
} // namespace ZShell::Services };
@@ -5,6 +5,7 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QDebug> #include <QDebug>
#include <QJsonArray>
namespace ZShell::services { namespace ZShell::services {
@@ -12,7 +13,7 @@ DesktopStateManager::DesktopStateManager(QObject *parent) : QObject(parent) {
} }
QString DesktopStateManager::getConfigFilePath() const { QString DesktopStateManager::getConfigFilePath() const {
QString configDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/sleex"; QString configDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/zshell";
QDir dir(configDir); QDir dir(configDir);
if (!dir.exists()) { if (!dir.exists()) {
dir.mkpath("."); dir.mkpath(".");
@@ -29,7 +30,7 @@ void DesktopStateManager::saveLayout(const QVariantMap& layout) {
file.write(doc.toJson(QJsonDocument::Indented)); file.write(doc.toJson(QJsonDocument::Indented));
file.close(); file.close();
} else { } else {
qWarning() << "Sleex: Impossible de sauvegarder le layout du bureau dans" << getConfigFilePath(); qWarning() << "zshell: Cannot save desktop layout to" << getConfigFilePath();
} }
} }