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) {
+211 -174
View File
@@ -6,227 +6,264 @@ import qs.Components
import qs.Config 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 bool targetIsDir: false
property var targetPaths: []
property string targetFilePath: "" signal openFileRequested(string path, bool isDir)
property bool targetIsDir: false signal renameRequested(string path)
property var targetAppEntry: null
property var targetPaths: [] function close() {
visible = false;
}
signal openFileRequested(string path, bool isDir) function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) {
signal renameRequested(string path) targetFilePath = path;
targetIsDir = isDir;
targetAppEntry = appEnt;
property real menuX: 0 targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path];
property real menuY: 0
CustomClippingRect { menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth));
id: popupBackground menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight));
readonly property real padding: Appearance.padding.small
x: contextMenu.menuX visible = true;
y: contextMenu.menuY }
color: DynamicColors.tPalette.m3surface anchors.fill: parent
radius: Appearance.rounding.normal visible: false
z: 999
implicitWidth: menuLayout.implicitWidth + padding * 2 CustomClippingRect {
implicitHeight: menuLayout.implicitHeight + padding * 2 id: popupBackground
Behavior on opacity { Anim {} } readonly property real padding: Appearance.padding.small
opacity: contextMenu.visible ? 1 : 0
ColumnLayout { color: DynamicColors.tPalette.m3surface
id: menuLayout implicitHeight: menuLayout.implicitHeight + padding * 2
anchors.centerIn: parent implicitWidth: menuLayout.implicitWidth + padding * 2
spacing: 0 opacity: contextMenu.visible ? 1 : 0
radius: Appearance.rounding.normal
x: contextMenu.menuX
y: contextMenu.menuY
CustomRect { Behavior on opacity {
Layout.preferredWidth: 160 Anim {
radius: popupBackground.radius - popupBackground.padding }
implicitHeight: openRow.implicitHeight + Appearance.padding.small * 2 }
RowLayout { ColumnLayout {
id: openRow id: menuLayout
spacing: 8
anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
MaterialIcon { text: "open_in_new"; font.pointSize: 20 } anchors.centerIn: parent
CustomText { text: "Open"; Layout.fillWidth: true } spacing: 0
}
StateLayer { CustomRect {
anchors.fill: parent Layout.preferredWidth: 160
implicitHeight: openRow.implicitHeight + Appearance.padding.small * 2
radius: popupBackground.radius - popupBackground.padding
onClicked: { RowLayout {
for (let i = 0; i < contextMenu.targetPaths.length; i++) { id: openRow
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 { anchors.fill: parent
Layout.fillWidth: true anchors.leftMargin: Appearance.padding.smaller
radius: popupBackground.radius - popupBackground.padding spacing: 8
implicitHeight: openWithRow.implicitHeight + Appearance.padding.small * 2
RowLayout { MaterialIcon {
id: openWithRow font.pointSize: 20
spacing: 8 text: "open_in_new"
anchors.fill: parent }
anchors.leftMargin: Appearance.padding.smaller
MaterialIcon { text: contextMenu.targetIsDir ? "terminal" : "apps"; font.pointSize: 20 } CustomText {
CustomText { text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."; Layout.fillWidth: true } Layout.fillWidth: true
} text: "Open"
}
}
StateLayer { StateLayer {
anchors.fill: parent 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: { 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 Layout.bottomMargin: 4
implicitHeight: 1 Layout.fillWidth: true
color: DynamicColors.palette.m3outlineVariant Layout.topMargin: 4
Layout.topMargin: 4 color: DynamicColors.palette.m3outlineVariant
Layout.bottomMargin: 4 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.leftMargin: Appearance.padding.smaller
MaterialIcon { text: "content_copy"; font.pointSize: 20 } anchors.fill: parent
CustomText { text: "Copy path"; Layout.fillWidth: true } anchors.leftMargin: Appearance.padding.smaller
} spacing: 8
StateLayer { MaterialIcon {
anchors.fill: parent font.pointSize: 20
text: "content_copy"
}
onClicked: { CustomText {
Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]) Layout.fillWidth: true
contextMenu.close() text: "Copy path"
} }
} }
}
CustomRect { StateLayer {
Layout.fillWidth: true anchors.fill: parent
visible: contextMenu.targetPaths.length === 1
radius: popupBackground.radius - popupBackground.padding
implicitHeight: renameRow.implicitHeight + Appearance.padding.small * 2
RowLayout { onClicked: {
id: renameRow Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]);
spacing: 8 contextMenu.close();
anchors.fill: parent }
anchors.leftMargin: Appearance.padding.smaller }
}
MaterialIcon { text: "edit"; font.pointSize: 20 } CustomRect {
CustomText { text: "Rename"; Layout.fillWidth: true } Layout.fillWidth: true
} implicitHeight: renameRow.implicitHeight + Appearance.padding.small * 2
radius: popupBackground.radius - popupBackground.padding
visible: contextMenu.targetPaths.length === 1
StateLayer { RowLayout {
anchors.fill: parent id: renameRow
onClicked: { anchors.fill: parent
contextMenu.renameRequested(contextMenu.targetFilePath) anchors.leftMargin: Appearance.padding.smaller
contextMenu.close() spacing: 8
}
}
}
Rectangle { MaterialIcon {
Layout.fillWidth: true font.pointSize: 20
implicitHeight: 1 text: "edit"
color: DynamicColors.palette.m3outlineVariant }
Layout.topMargin: 4
Layout.bottomMargin: 4
}
CustomRect { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
radius: popupBackground.radius - popupBackground.padding text: "Rename"
implicitHeight: deleteRow.implicitHeight + Appearance.padding.small * 2 }
}
RowLayout { StateLayer {
id: deleteRow anchors.fill: parent
spacing: 8
anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
MaterialIcon { onClicked: {
text: "delete" contextMenu.renameRequested(contextMenu.targetFilePath);
font.pointSize: 20 contextMenu.close();
color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error }
} }
}
CustomText { Rectangle {
text: "Move to trash" Layout.bottomMargin: 4
Layout.fillWidth: true Layout.fillWidth: true
color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error Layout.topMargin: 4
} color: DynamicColors.palette.m3outlineVariant
} implicitHeight: 1
}
StateLayer { CustomRect {
id: deleteButton Layout.fillWidth: true
anchors.fill: parent implicitHeight: deleteRow.implicitHeight + Appearance.padding.small * 2
color: DynamicColors.tPalette.m3error radius: popupBackground.radius - popupBackground.padding
onClicked: { RowLayout {
let cmd = ["gio", "trash"].concat(contextMenu.targetPaths) id: deleteRow
Quickshell.execDetached(cmd)
contextMenu.close()
}
}
}
}
}
function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) { anchors.fill: parent
targetFilePath = path anchors.leftMargin: Appearance.padding.smaller
targetIsDir = isDir spacing: 8
targetAppEntry = appEnt
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)) CustomText {
menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight)) Layout.fillWidth: true
color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error
text: "Move to trash"
}
}
visible = true StateLayer {
} id: deleteButton
function close() { anchors.fill: parent
visible = false color: DynamicColors.tPalette.m3error
}
onClicked: {
let cmd = ["gio", "trash"].concat(contextMenu.targetPaths);
Quickshell.execDetached(cmd);
contextMenu.close();
}
}
}
}
}
} }
+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();
} }
} }